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) => {
|
||||
const route = getRouteUrl(props.children.props);
|
||||
const { setDesignable } = useDesignable();
|
||||
const { render } = useAppSpin();
|
||||
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,
|
||||
} from 'react-router-dom';
|
||||
import { Application } from './Application';
|
||||
import { CustomRouterContextProvider } from './CustomRouterContextProvider';
|
||||
import { BlankComponent, RouterContextCleaner } from './components';
|
||||
|
||||
export interface BrowserRouterOptions extends Omit<BrowserRouterProps, 'children'> {
|
||||
@ -143,10 +144,12 @@ export class RouterManager {
|
||||
return (
|
||||
<RouterContextCleaner>
|
||||
<ReactRouter {...opts}>
|
||||
<CustomRouterContextProvider>
|
||||
<BaseLayout>
|
||||
<RenderRoutes />
|
||||
{children}
|
||||
</BaseLayout>
|
||||
</CustomRouterContextProvider>
|
||||
</ReactRouter>
|
||||
</RouterContextCleaner>
|
||||
);
|
||||
|
@ -8,12 +8,16 @@
|
||||
*/
|
||||
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useApp } from './useApp';
|
||||
|
||||
export const useAppSpin = () => {
|
||||
const app = useApp();
|
||||
const renderSpin = useCallback(
|
||||
() => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)),
|
||||
[app],
|
||||
);
|
||||
return {
|
||||
render: () => (app?.renderComponent ? app?.renderComponent?.('AppSpin') : React.createElement(Spin)),
|
||||
render: renderSpin,
|
||||
};
|
||||
};
|
||||
|
@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
export * from './Application';
|
||||
export * from './CustomRouterContextProvider';
|
||||
export * from './Plugin';
|
||||
export * from './PluginSettingsManager';
|
||||
export * from './RouterManager';
|
||||
@ -19,5 +20,8 @@ export * from './schema-initializer';
|
||||
export * from './schema-settings';
|
||||
export * from './schema-settings/context/SchemaSettingItemContext';
|
||||
export * from './schema-settings/hooks/useSchemaSettingsRender';
|
||||
export * from './schema-settings/utils/createModalSettingsItem';
|
||||
export * from './schema-settings/utils/createSelectSettingsItem';
|
||||
export * from './schema-settings/utils/createSwitchSettingsItem';
|
||||
export * from './schema-toolbar';
|
||||
export * from './utils';
|
||||
|
@ -12,4 +12,3 @@ export * from './SchemaSettingsManager';
|
||||
export * from './components';
|
||||
export * from './context/SchemaSettingItemContext';
|
||||
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,
|
||||
useCollectionParentRecord,
|
||||
useCollectionRecord,
|
||||
useCollectionRecordData,
|
||||
useDataBlockProps,
|
||||
useDataBlockRequest,
|
||||
useDataBlockResource,
|
||||
@ -35,6 +36,7 @@ import {
|
||||
import { DataBlockCollector } from '../filter-provider/FilterProvider';
|
||||
import { useSourceId } from '../modules/blocks/useSourceId';
|
||||
import { RecordProvider, useRecordIndex } from '../record-provider';
|
||||
import { usePagePopup } from '../schema-component/antd/page/pagePopupUtils';
|
||||
import { useAssociationNames } from './hooks';
|
||||
import { useDataBlockParentRecord } from './hooks/useDataBlockParentRecord';
|
||||
|
||||
@ -293,11 +295,17 @@ export const useBlockAssociationContext = () => {
|
||||
export const useFilterByTk = () => {
|
||||
const { resource, __parent } = useBlockRequestContext();
|
||||
const recordIndex = useRecordIndex();
|
||||
const record = useRecord();
|
||||
const recordData = useCollectionRecordData();
|
||||
const collection = useCollection_deprecated();
|
||||
const { getCollectionField } = useCollectionManager_deprecated();
|
||||
const assoc = useBlockAssociationContext();
|
||||
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
|
||||
const { popupParams } = usePagePopup();
|
||||
|
||||
if (popupParams?.filterbytk) {
|
||||
return popupParams.filterbytk;
|
||||
}
|
||||
|
||||
if (!withoutTableFieldResource) {
|
||||
if (resource instanceof TableFieldResource || __parent?.block === 'TableField') {
|
||||
return recordIndex;
|
||||
@ -306,9 +314,9 @@ export const useFilterByTk = () => {
|
||||
|
||||
if (assoc) {
|
||||
const association = getCollectionField(assoc);
|
||||
return record?.[association.targetKey || 'id'];
|
||||
return recordData?.[association.targetKey || 'id'];
|
||||
}
|
||||
return record?.[collection.filterTargetKey || 'id'];
|
||||
return recordData?.[collection.filterTargetKey || 'id'];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -154,10 +154,8 @@ export const useFormBlockContext = () => {
|
||||
export const useFormBlockProps = () => {
|
||||
const ctx = useFormBlockContext();
|
||||
const treeParentRecord = useTreeParentRecord();
|
||||
const { fieldSchema } = useActionContext();
|
||||
const addChild = fieldSchema?.['x-component-props']?.addChild;
|
||||
useEffect(() => {
|
||||
if (addChild) {
|
||||
if (treeParentRecord) {
|
||||
ctx.form?.query('parent').take((field) => {
|
||||
field.disabled = true;
|
||||
field.value = treeParentRecord;
|
||||
|
@ -159,7 +159,7 @@ export const TableBlockProvider = withDynamicSchemaProps((props) => {
|
||||
}
|
||||
params['tree'] = true;
|
||||
} else {
|
||||
const f = collection.fields.find((f) => f.treeChildren);
|
||||
const f = collection?.fields.find((f) => f.treeChildren);
|
||||
if (f) {
|
||||
childrenColumnName = f.name;
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ import omit from 'lodash/omit';
|
||||
import qs from 'qs';
|
||||
import { ChangeEvent, useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useReactToPrint } from 'react-to-print';
|
||||
import {
|
||||
AssociationFilter,
|
||||
@ -30,6 +29,7 @@ import {
|
||||
useTableBlockContext,
|
||||
} from '../..';
|
||||
import { useAPIClient, useRequest } from '../../api-client';
|
||||
import { useNavigateNoUpdate } from '../../application/CustomRouterContextProvider';
|
||||
import { useFormBlockContext } from '../../block-provider/FormBlockProvider';
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
||||
import { DataBlock, useFilterBlock } from '../../filter-provider/FilterProvider';
|
||||
@ -206,7 +206,7 @@ export const useCreateActionProps = () => {
|
||||
const form = useForm();
|
||||
const { field, resource } = useBlockRequestContext();
|
||||
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const actionSchema = useFieldSchema();
|
||||
const actionField = useField();
|
||||
const compile = useCompile();
|
||||
@ -548,7 +548,7 @@ export const useCustomizeUpdateActionProps = () => {
|
||||
const { resource, __parent, service } = useBlockRequestContext();
|
||||
const filterByTk = useFilterByTk();
|
||||
const actionSchema = useFieldSchema();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const compile = useCompile();
|
||||
const form = useForm();
|
||||
const { modal } = App.useApp();
|
||||
@ -643,7 +643,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
||||
const { rowKey } = tableBlockContext;
|
||||
const selectedRecordKeys =
|
||||
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const actionField = useField();
|
||||
@ -754,7 +754,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
||||
|
||||
export const useCustomizeRequestActionProps = () => {
|
||||
const apiClient = useAPIClient();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const filterByTk = useFilterByTk();
|
||||
const actionSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
@ -850,7 +850,7 @@ export const useUpdateActionProps = () => {
|
||||
const { field, resource, __parent } = useBlockRequestContext();
|
||||
const { setVisible, setSubmitted, setFormValueChanged } = useActionContext();
|
||||
const actionSchema = useFieldSchema();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const { fields, getField, name } = useCollection_deprecated();
|
||||
const compile = useCompile();
|
||||
const actionField = useField();
|
||||
@ -1530,7 +1530,7 @@ export function appendQueryStringToUrl(url: string, queryString: string) {
|
||||
}
|
||||
|
||||
export function useLinkActionProps() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const url = fieldSchema?.['x-component-props']?.['url'];
|
||||
|
@ -73,7 +73,7 @@ export function useParsedFilter({ filterOption }: { filterOption: any }) {
|
||||
equals: _.isEqual,
|
||||
},
|
||||
);
|
||||
}, [JSON.stringify(filterOption)]);
|
||||
}, [JSON.stringify(filterOption), parseFilter, findVariable]);
|
||||
|
||||
return {
|
||||
/** 数据范围的筛选参数 */
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useEffect } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||
@ -23,10 +23,7 @@ const CollectionHistoryContext = createContext<CollectionHistoryContextValue>({
|
||||
});
|
||||
CollectionHistoryContext.displayName = 'CollectionHistoryContext';
|
||||
|
||||
export const CollectionHistoryProvider: React.FC = (props) => {
|
||||
const api = useAPIClient();
|
||||
|
||||
const options = {
|
||||
const options = {
|
||||
resource: 'collectionsHistory',
|
||||
action: 'list',
|
||||
params: {
|
||||
@ -37,12 +34,12 @@ export const CollectionHistoryProvider: React.FC = (props) => {
|
||||
},
|
||||
sort: ['sort'],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const CollectionHistoryProvider: React.FC = (props) => {
|
||||
const api = useAPIClient();
|
||||
const location = useLocation();
|
||||
|
||||
// console.log('location', location);
|
||||
|
||||
const isAdminPage = location.pathname.startsWith('/admin');
|
||||
const token = api.auth.getToken() || '';
|
||||
const { render } = useAppSpin();
|
||||
@ -55,26 +52,24 @@ export const CollectionHistoryProvider: React.FC = (props) => {
|
||||
});
|
||||
|
||||
// 刷新 collecionHistory
|
||||
const refreshCH = async () => {
|
||||
const refreshCH = useCallback(async () => {
|
||||
const { data } = await api.request(options);
|
||||
service.mutate(data);
|
||||
return data?.data || [];
|
||||
}, [service]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
historyCollections: service.data?.data,
|
||||
refreshCH,
|
||||
};
|
||||
}, [refreshCH, service.data?.data]);
|
||||
|
||||
if (service.loading) {
|
||||
return render();
|
||||
}
|
||||
|
||||
return (
|
||||
<CollectionHistoryContext.Provider
|
||||
value={{
|
||||
historyCollections: service.data?.data,
|
||||
refreshCH,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</CollectionHistoryContext.Provider>
|
||||
);
|
||||
return <CollectionHistoryContext.Provider value={value}>{props.children}</CollectionHistoryContext.Provider>;
|
||||
};
|
||||
|
||||
export const useHistoryCollectionsByNames = (collectionNames: string[]) => {
|
||||
|
@ -7,15 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
|
||||
import { CollectionCategroriesContext } from './context';
|
||||
import { CollectionManagerOptions } from './types';
|
||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||
import { CollectionManagerProvider } from '../data-source/collection/CollectionManagerProvider';
|
||||
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
|
||||
import { useCollectionHistory } from './CollectionHistoryProvider';
|
||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||
import { CollectionManagerSchemaComponentProvider } from './CollectionManagerSchemaComponentProvider';
|
||||
import { CollectionCategroriesContext } from './context';
|
||||
import { CollectionManagerOptions } from './types';
|
||||
|
||||
/**
|
||||
* @deprecated use `CollectionManagerProvider` instead
|
||||
@ -28,18 +28,19 @@ export const CollectionManagerProvider_deprecated: React.FC<CollectionManagerOpt
|
||||
);
|
||||
};
|
||||
|
||||
export const RemoteCollectionManagerProvider = (props: any) => {
|
||||
const api = useAPIClient();
|
||||
const dm = useDataSourceManager();
|
||||
const { refreshCH } = useCollectionHistory();
|
||||
|
||||
const coptions = {
|
||||
const coptions = {
|
||||
url: 'collectionCategories:list',
|
||||
params: {
|
||||
paginate: false,
|
||||
sort: ['sort'],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const RemoteCollectionManagerProvider = (props: any) => {
|
||||
const api = useAPIClient();
|
||||
const dm = useDataSourceManager();
|
||||
const { refreshCH } = useCollectionHistory();
|
||||
|
||||
const service = useRequest<{
|
||||
data: any;
|
||||
}>(() => {
|
||||
@ -50,17 +51,18 @@ export const RemoteCollectionManagerProvider = (props: any) => {
|
||||
}>(coptions);
|
||||
|
||||
const { render } = useAppSpin();
|
||||
const refreshCategory = useCallback(async () => {
|
||||
const { data } = await api.request(coptions);
|
||||
result.mutate(data);
|
||||
return data?.data || [];
|
||||
}, [result]);
|
||||
|
||||
if (service.loading) {
|
||||
return render();
|
||||
}
|
||||
|
||||
const refreshCategory = async () => {
|
||||
const { data } = await api.request(coptions);
|
||||
result.mutate(data);
|
||||
return data?.data || [];
|
||||
};
|
||||
return (
|
||||
<CollectionCategroriesProvider service={{ ...result }} refreshCategory={refreshCategory}>
|
||||
<CollectionCategroriesProvider service={result} refreshCategory={refreshCategory}>
|
||||
<CollectionManagerProvider_deprecated {...props}></CollectionManagerProvider_deprecated>
|
||||
</CollectionCategroriesProvider>
|
||||
);
|
||||
|
@ -9,21 +9,22 @@
|
||||
|
||||
import React from 'react';
|
||||
import { SchemaComponentOptions } from '..';
|
||||
import { CollectionProvider_deprecated, ResourceActionProvider, useDataSourceFromRAC } from '.';
|
||||
import { CollectionProvider_deprecated } from './CollectionProvider_deprecated';
|
||||
import { ResourceActionProvider, useDataSourceFromRAC } from './ResourceActionProvider';
|
||||
import * as hooks from './action-hooks';
|
||||
import { DataSourceProvider_deprecated, ds, SubFieldDataSourceProvider_deprecated } from './sub-table';
|
||||
import { DataSourceProvider_deprecated, SubFieldDataSourceProvider_deprecated, ds } from './sub-table';
|
||||
|
||||
export const CollectionManagerSchemaComponentProvider: React.FC = (props) => {
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
scope={{ cm: { ...hooks, useDataSourceFromRAC }, ds }}
|
||||
components={{
|
||||
const scope = { cm: { ...hooks, useDataSourceFromRAC }, ds };
|
||||
const components = {
|
||||
SubFieldDataSourceProvider_deprecated,
|
||||
DataSourceProvider_deprecated,
|
||||
CollectionProvider_deprecated,
|
||||
ResourceActionProvider,
|
||||
}}
|
||||
>
|
||||
};
|
||||
|
||||
export const CollectionManagerSchemaComponentProvider: React.FC = (props) => {
|
||||
return (
|
||||
<SchemaComponentOptions scope={scope} components={components}>
|
||||
{props.children}
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
|
@ -7,17 +7,17 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { uid } from '@formily/shared';
|
||||
import { CascaderProps } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCompile, useSchemaComponentContext } from '../../schema-component';
|
||||
import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types';
|
||||
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
|
||||
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
|
||||
import { useDataSourceManager } from '../../data-source/data-source/DataSourceManagerProvider';
|
||||
import { useDataSource } from '../../data-source/data-source/DataSourceProvider';
|
||||
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
|
||||
import { useCollectionManager } from '../../data-source/collection/CollectionManagerProvider';
|
||||
import { useCompile, useSchemaComponentContext } from '../../schema-component';
|
||||
import { InheritanceCollectionMixin } from '../mixins/InheritanceCollectionMixin';
|
||||
import { CollectionFieldOptions_deprecated, CollectionOptions } from '../types';
|
||||
|
||||
/**
|
||||
* @deprecated use `useCollectionManager` instead
|
||||
@ -273,9 +273,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
||||
);
|
||||
|
||||
// 是否可以作为标题字段
|
||||
const isTitleField = (field) => {
|
||||
const isTitleField = useCallback(
|
||||
(field) => {
|
||||
return getInterface(field.interface)?.titleUsable;
|
||||
};
|
||||
},
|
||||
[getInterface],
|
||||
);
|
||||
|
||||
const getParentCollectionFields = useCallback(
|
||||
(parentCollection, currentCollection, customDataSource?: string) => {
|
||||
|
@ -7,7 +7,6 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import React, { FC, ReactNode, createContext, useContext, useMemo } from 'react';
|
||||
|
||||
import { ACLCollectionProvider } from '../../acl/ACLProvider';
|
||||
|
@ -38,7 +38,7 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
|
||||
}
|
||||
const paramsValue = params.filterByTk === undefined ? _.omit(params, 'filterByTk') : params;
|
||||
|
||||
return resource[action]({ ...paramsValue, ...customParams }).then((res) => res.data);
|
||||
return resource[action]?.({ ...paramsValue, ...customParams }).then((res) => res.data);
|
||||
};
|
||||
}, [resource, action, JSON.stringify(params), JSON.stringify(record), requestService]);
|
||||
|
||||
@ -129,7 +129,16 @@ export const BlockRequestProvider: FC = ({ children }) => {
|
||||
});
|
||||
|
||||
const memoizedParentRecord = useMemo(() => {
|
||||
return parentRequest.data?.data && new CollectionRecord({ isNew: false, data: parentRequest.data?.data });
|
||||
return (
|
||||
parentRequest.data?.data &&
|
||||
new CollectionRecord({
|
||||
isNew: false,
|
||||
data:
|
||||
parentRequest.data?.data instanceof CollectionRecord
|
||||
? parentRequest.data?.data.data
|
||||
: parentRequest.data?.data,
|
||||
})
|
||||
);
|
||||
}, [parentRequest.data?.data]);
|
||||
|
||||
return (
|
||||
@ -137,13 +146,13 @@ export const BlockRequestProvider: FC = ({ children }) => {
|
||||
{action !== 'list' ? (
|
||||
<CollectionRecordProvider
|
||||
isNew={action == null}
|
||||
record={currentRequest.data?.data}
|
||||
parentRecord={memoizedParentRecord}
|
||||
record={currentRequest.data?.data || record}
|
||||
parentRecord={memoizedParentRecord || parentRecord}
|
||||
>
|
||||
{children}
|
||||
</CollectionRecordProvider>
|
||||
) : (
|
||||
<CollectionRecordProvider isNew={false} record={null} parentRecord={memoizedParentRecord}>
|
||||
<CollectionRecordProvider isNew={false} record={null} parentRecord={memoizedParentRecord || parentRecord}>
|
||||
{children}
|
||||
</CollectionRecordProvider>
|
||||
)}
|
||||
|
@ -40,7 +40,7 @@ export const DataBlockResourceProvider: FC<{ children?: ReactNode }> = ({ childr
|
||||
|
||||
const resource = useMemo(() => {
|
||||
if (association) {
|
||||
return api.resource(association, sourceIdValue, headers);
|
||||
return api.resource(association, sourceIdValue, headers, !sourceIdValue);
|
||||
}
|
||||
return api.resource(collectionName, undefined, headers);
|
||||
}, [api, association, collection, sourceIdValue, headers]);
|
||||
|
@ -40,7 +40,7 @@ export interface ForeignKeyField {
|
||||
type Collection = ReturnType<typeof useCollection_deprecated>;
|
||||
|
||||
export interface DataBlock {
|
||||
/** 唯一标识符,schema 中的 name 值 */
|
||||
/** 唯一标识符,schema 中的 x-uid 值 */
|
||||
uid: string;
|
||||
/** 用户自行设置的区块名称 */
|
||||
title?: string;
|
||||
|
@ -8,7 +8,8 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { OneTableWithDelete } from './templates';
|
||||
import { OneTableWithDelete, shouldRefreshBlockDataAfterMultiplePopupsClosed } from './templates';
|
||||
|
||||
test.describe('action settings', () => {
|
||||
test('refresh data on action', async ({ page, mockPage, mockRecords }) => {
|
||||
await mockPage(OneTableWithDelete).goto();
|
||||
@ -44,4 +45,24 @@ test.describe('action settings', () => {
|
||||
await page.waitForTimeout(500);
|
||||
expect(requestMade).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should refresh block data after multiple popups closed', async ({ page, mockPage, mockRecord }) => {
|
||||
const nocoPage = await mockPage(shouldRefreshBlockDataAfterMultiplePopupsClosed).waitForInit();
|
||||
await mockRecord('users', { username: 'Test', roles: [{ title: 'Test role' }] });
|
||||
await nocoPage.goto();
|
||||
|
||||
await page.getByLabel('action-Action.Link-Edit-update-users-table-1').click();
|
||||
await page.getByTestId('drawer-Action.Container-users-Edit record').getByLabel('action-Action.Link-Edit-').click();
|
||||
await page.getByLabel('block-item-CollectionField-').getByRole('textbox').fill('abc123');
|
||||
await page.getByLabel('action-Action-Submit-submit-').click();
|
||||
|
||||
// the first popup
|
||||
await expect(
|
||||
page.getByTestId('drawer-Action.Container-users-Edit record').getByRole('button', { name: 'abc123' }),
|
||||
).toBeVisible();
|
||||
|
||||
// close the first popup
|
||||
await page.getByLabel('drawer-Action.Container-users-Edit record-mask').click();
|
||||
await expect(page.getByLabel('block-item-CardItem-users-').getByRole('button', { name: 'abc123' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import { expect, test } from '@nocobase/test/e2e';
|
||||
import { oneEmptyTableWithUsers } from './templates';
|
||||
import { PopupAndSubPageWithParams, oneEmptyTableWithUsers } from './templates';
|
||||
|
||||
test.describe('Link', () => {
|
||||
test('basic', async ({ page, mockPage, mockRecords }) => {
|
||||
@ -84,4 +84,30 @@ test.describe('Link', () => {
|
||||
await expect(page.getByRole('button', { name: 'nocobase', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: users[1].username, exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('popup and sub page with search params', async ({ page, mockPage, mockRecords }) => {
|
||||
const nocoPage = mockPage(PopupAndSubPageWithParams);
|
||||
const url = await nocoPage.getUrl();
|
||||
await page.goto(url + '?name=abc');
|
||||
|
||||
// open popup with drawer mode
|
||||
await page.getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc');
|
||||
|
||||
await page.goBack();
|
||||
await page.getByLabel('action-Action.Link-View-view-').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
|
||||
await page.getByRole('option', { name: 'Page' }).click();
|
||||
|
||||
// open sub page with page mode
|
||||
await page.getByLabel('action-Action.Link-View-view-').click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('abc');
|
||||
});
|
||||
});
|
||||
|
@ -222,3 +222,368 @@ export const oneEmptyTableWithUsers = {
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
export const PopupAndSubPageWithParams = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
b1atmxyqbff: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {
|
||||
rkebzj98nvq: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
kxz1o673sem: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
t79t7o2scw5: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-uid': 'm3lmb6s616o',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
sghz67ot4pa: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-toolbar-props': {
|
||||
initializer: 'table:configureItemActions',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
ebq18cea3uo: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
c14cpxkc4ke: {
|
||||
'x-uid': 'ech5j6ogus0',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("View") }}',
|
||||
'x-action': 'view',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:view',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
},
|
||||
'x-action-context': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer-props': {
|
||||
linkageAction: true,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("View record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Details")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'popup:common:addBlock',
|
||||
properties: {
|
||||
z1h7wnh6s60: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
bxzkcif7vu9: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
e2pjbwaou03: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props': {
|
||||
skipScopeCheck: true,
|
||||
},
|
||||
'x-acl-action': 'users:create',
|
||||
'x-decorator': 'FormBlockProvider',
|
||||
'x-use-decorator-props':
|
||||
'useCreateFormBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
isCusomeizeCreate: true,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:createForm',
|
||||
'x-component': 'CardItem',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
j86fmzva2n0: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'FormV2',
|
||||
'x-use-component-props': 'useCreateFormBlockProps',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'form:configureFields',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
s1cu9qfbyx6: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
rmck9ducuw7: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
nickname: {
|
||||
'x-uid': '22a0rhtgo1x',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'string',
|
||||
'x-toolbar':
|
||||
'FormItemSchemaToolbar',
|
||||
'x-settings':
|
||||
'fieldSettings:FormItem',
|
||||
'x-component': 'CollectionField',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-collection-field':
|
||||
'users.nickname',
|
||||
'x-component-props': {},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
default:
|
||||
'{{$nURLSearchParams.name}}',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'eudbw9veltu',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'jdt8bqzqi2s',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '5hq25kdeet9',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
kke3cg70wha: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'createForm:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
layout: 'one-column',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-uid': 'usbcj3rxwdq',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'xk49vnplykb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'xz0fzo8aiwd',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'c2z7nkp1er8',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '817scrzocq2',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'o5aa5p4qdfy',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'cyys2ghczmx',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'pte28pwapoh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'nnc6fz5v38k',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'nba9caa8pc1',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '14vqyxwyyty',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'tu8n9op1z9r',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'v2gig2bsyex',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '9r7wta3gbml',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'zj3df6jm0dh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'fz1jj9rlu87',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'de0k4foh9cc',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
|
@ -196,3 +196,780 @@ export const OneTableWithDelete = {
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
export const shouldRefreshBlockDataAfterMultiplePopupsClosed = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
properties: {
|
||||
tyx0aioxlr1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
properties: {
|
||||
'0i5raczm0ne': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
zjk1te2zpix: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
k7eyfysw52n: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
collection: 'users',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'id',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-uid': 'fkgbia9i3kt',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
pn8pb7x81kh: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-toolbar-props': {
|
||||
initializer: 'table:configureItemActions',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
g3flunks833: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
noihi5pm44q: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:edit',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-action-context': {
|
||||
dataSource: 'main',
|
||||
collection: 'users',
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer-props': {
|
||||
linkageAction: true,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component': 'Tabs.TabPane',
|
||||
'x-designer': 'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'popup:common:addBlock',
|
||||
properties: {
|
||||
suv1o9u1ti1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
oqzxplx2qap: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
uxrg8z6qz3w: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableBlockProvider',
|
||||
'x-acl-action': 'users.roles:list',
|
||||
'x-use-decorator-props': 'useTableBlockDecoratorProps',
|
||||
'x-decorator-props': {
|
||||
association: 'users.roles',
|
||||
dataSource: 'main',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 20,
|
||||
},
|
||||
rowKey: 'name',
|
||||
showIndex: true,
|
||||
dragSort: false,
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
'x-settings': 'blockSettings:table',
|
||||
'x-component': 'CardItem',
|
||||
'x-filter-targets': [],
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-initializer': 'table:configureActions',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 'var(--nb-spacing)',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-uid': 'nzb7njucb1g',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
prcxluma72r: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'array',
|
||||
'x-initializer': 'table:configureColumns',
|
||||
'x-component': 'TableV2',
|
||||
'x-use-component-props': 'useTableBlockProps',
|
||||
'x-component-props': {
|
||||
rowKey: 'id',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
actions: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-action-column': 'actions',
|
||||
'x-decorator': 'TableV2.Column.ActionBar',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-initializer': 'table:configureItemActions',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-toolbar-props': {
|
||||
initializer: 'table:configureItemActions',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
z3n08qgeo8y: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'DndContext',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
pla7f5mwirx: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
'x-action': 'update',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:edit',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
openMode: 'drawer',
|
||||
icon: 'EditOutlined',
|
||||
},
|
||||
'x-action-context': {
|
||||
dataSource: 'main',
|
||||
association: 'users.roles',
|
||||
sourceId: 1,
|
||||
parentPopupRecord: {
|
||||
collection: 'users',
|
||||
filterByTk: 1,
|
||||
},
|
||||
},
|
||||
'x-decorator': 'ACLActionProvider',
|
||||
'x-designer-props': {
|
||||
linkageAction: true,
|
||||
},
|
||||
properties: {
|
||||
drawer: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-component': 'Action.Container',
|
||||
'x-component-props': {
|
||||
className: 'nb-action-popup',
|
||||
},
|
||||
properties: {
|
||||
tabs: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Tabs',
|
||||
'x-component-props': {},
|
||||
'x-initializer': 'popup:addTab',
|
||||
properties: {
|
||||
tab1: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{t("Edit")}}',
|
||||
'x-component':
|
||||
'Tabs.TabPane',
|
||||
'x-designer':
|
||||
'Tabs.Designer',
|
||||
'x-component-props': {},
|
||||
properties: {
|
||||
grid: {
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer':
|
||||
'popup:common:addBlock',
|
||||
properties: {
|
||||
k6k23ljfmwv: {
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'Grid.Row',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties: {
|
||||
ddkwboqqe19: {
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'Grid.Col',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties: {
|
||||
'33h5pkdwmt5':
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-acl-action-props':
|
||||
{
|
||||
skipScopeCheck:
|
||||
false,
|
||||
},
|
||||
'x-acl-action':
|
||||
'users.roles:update',
|
||||
'x-decorator':
|
||||
'FormBlockProvider',
|
||||
'x-use-decorator-props':
|
||||
'useEditFormBlockDecoratorProps',
|
||||
'x-decorator-props':
|
||||
{
|
||||
action:
|
||||
'get',
|
||||
dataSource:
|
||||
'main',
|
||||
association:
|
||||
'users.roles',
|
||||
},
|
||||
'x-toolbar':
|
||||
'BlockSchemaToolbar',
|
||||
'x-settings':
|
||||
'blockSettings:editForm',
|
||||
'x-component':
|
||||
'CardItem',
|
||||
'x-is-current':
|
||||
true,
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
'7t3234fb1yu':
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'FormV2',
|
||||
'x-use-component-props':
|
||||
'useEditFormBlockProps',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
grid: {
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'Grid',
|
||||
'x-initializer':
|
||||
'form:configureFields',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
xizv324ooej:
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'Grid.Row',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
gjzwqen45hw:
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-component':
|
||||
'Grid.Col',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
title:
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'string',
|
||||
'x-toolbar':
|
||||
'FormItemSchemaToolbar',
|
||||
'x-settings':
|
||||
'fieldSettings:FormItem',
|
||||
'x-component':
|
||||
'CollectionField',
|
||||
'x-decorator':
|
||||
'FormItem',
|
||||
'x-collection-field':
|
||||
'roles.title',
|
||||
'x-component-props':
|
||||
{},
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
'x-uid':
|
||||
'i1q5apprfo3',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'cyrc9goozvc',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'1yonjmmam2w',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'yzom6z8f3t2',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
u0l41h24oqc:
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
type: 'void',
|
||||
'x-initializer':
|
||||
'editForm:configureActions',
|
||||
'x-component':
|
||||
'ActionBar',
|
||||
'x-component-props':
|
||||
{
|
||||
layout:
|
||||
'one-column',
|
||||
},
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
properties:
|
||||
{
|
||||
se7ggsy8wv0:
|
||||
{
|
||||
_isJSONSchemaObject:
|
||||
true,
|
||||
version:
|
||||
'2.0',
|
||||
title:
|
||||
'{{ t("Submit") }}',
|
||||
'x-action':
|
||||
'submit',
|
||||
'x-component':
|
||||
'Action',
|
||||
'x-use-component-props':
|
||||
'useUpdateActionProps',
|
||||
'x-toolbar':
|
||||
'ActionSchemaToolbar',
|
||||
'x-settings':
|
||||
'actionSettings:updateSubmit',
|
||||
'x-component-props':
|
||||
{
|
||||
type: 'primary',
|
||||
htmlType:
|
||||
'submit',
|
||||
},
|
||||
'x-action-settings':
|
||||
{
|
||||
triggerWorkflows:
|
||||
[],
|
||||
},
|
||||
type: 'void',
|
||||
'x-app-version':
|
||||
'1.2.11-alpha',
|
||||
'x-uid':
|
||||
'38c98jagvqk',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'4s6eki715ub',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'xnnwdz56xzc',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'zvr9ezaqhn8',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'l3xw7vpnnmi',
|
||||
'x-async':
|
||||
false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid':
|
||||
'mydjjzt5wjb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '1a12yaxkvlh',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '28wuyvfuzxa',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '739bd4xxp5r',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'sc3bt4kz43j',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'bxmc5k79nhw',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '4hz3jkt2wfo',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'x6x0ygv9b0s',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
lyn5o91qf9v: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
title: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-collection-field': 'roles.title',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
ellipsis: true,
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': null,
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-uid': 'a47ajmlmdq4',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'v3uq1qkh0f1',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'qxusos5qytp',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'skdhbz3b5wb',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '9keek6gthp6',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'n6k49p6zvw5',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'sod8zbrugmq',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'lt3vak0nmab',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'g1ee1hpkd4p',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'p4enijxueeg',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'bxd64x3df56',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '0y4f0f650h9',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '2qk92ft951c',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
fv22e8igzgz: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-decorator': 'TableV2.Column.Decorator',
|
||||
'x-toolbar': 'TableColumnSchemaToolbar',
|
||||
'x-settings': 'fieldSettings:TableColumn',
|
||||
'x-component': 'TableV2.Column',
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
properties: {
|
||||
roles: {
|
||||
'x-uid': 'zkenfmew3f7',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
'x-collection-field': 'users.roles',
|
||||
'x-component': 'CollectionField',
|
||||
'x-component-props': {
|
||||
fieldNames: {
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
},
|
||||
ellipsis: true,
|
||||
size: 'small',
|
||||
},
|
||||
'x-read-pretty': true,
|
||||
'x-decorator': null,
|
||||
'x-decorator-props': {
|
||||
labelStyle: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
'x-app-version': '1.2.11-alpha',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': '2fmv2b2n8it',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'n48nmuoljua',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-uid': 'oiu3t5hup6s',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'kc32hfy3m57',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'aayu9ix6qh6',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'vutamnve682',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'da87s7d7xs0',
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
};
|
||||
|
@ -8,8 +8,11 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
export const CreateChildInitializer = (props) => {
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{ t("Add child") }}',
|
||||
@ -63,6 +66,7 @@ export const CreateChildInitializer = (props) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||
};
|
||||
return <ActionInitializerItem {...props} schema={schema} />;
|
||||
};
|
||||
|
@ -9,9 +9,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useSchemaInitializerItem } from '../../../application';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
|
||||
export const CreateActionInitializer = () => {
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
'x-action': 'create',
|
||||
@ -65,6 +68,7 @@ export const CreateActionInitializer = () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||
};
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
return <ActionInitializerItem {...itemConfig} item={itemConfig} schema={schema} />;
|
||||
|
@ -9,9 +9,12 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useSchemaInitializerItem } from '../../../application';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { BlockInitializer } from '../../../schema-initializer/items';
|
||||
|
||||
export const PopupActionInitializer = (props) => {
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{ t("Popup") }}',
|
||||
@ -58,6 +61,7 @@ export const PopupActionInitializer = (props) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||
};
|
||||
|
||||
const itemConfig = useSchemaInitializerItem();
|
||||
|
@ -8,9 +8,12 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
|
||||
export const UpdateActionInitializer = (props) => {
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
@ -57,6 +60,7 @@ export const UpdateActionInitializer = (props) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||
};
|
||||
return <ActionInitializerItem {...props} schema={schema} />;
|
||||
};
|
||||
|
@ -8,9 +8,12 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { usePagePopup } from '../../../schema-component/antd/page/pagePopupUtils';
|
||||
import { CONTEXT_SCHEMA_KEY } from '../../../schema-component/antd/page/usePopupContextInActionOrAssociationField';
|
||||
import { ActionInitializerItem } from '../../../schema-initializer/items/ActionInitializerItem';
|
||||
|
||||
export const ViewActionInitializer = (props) => {
|
||||
const { getPopupContext } = usePagePopup();
|
||||
const schema = {
|
||||
type: 'void',
|
||||
title: '{{ t("View") }}',
|
||||
@ -56,6 +59,7 @@ export const ViewActionInitializer = (props) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[CONTEXT_SCHEMA_KEY]: getPopupContext(),
|
||||
};
|
||||
return <ActionInitializerItem {...props} schema={schema} />;
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ test.describe('where single data details block can be added', () => {
|
||||
await nocoPage.goto();
|
||||
|
||||
// 1.打开弹窗
|
||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover();
|
||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
||||
|
||||
// 2.通过 Current record 创建一个详情区块
|
||||
@ -121,7 +122,7 @@ test.describe('configure actions', () => {
|
||||
|
||||
// create delete ------------------------------------------------------------------------------------
|
||||
await createAction(page, 'Delete');
|
||||
await expect(page.getByLabel('action-Action-Delete-destroy-general-details-')).toBeVisible();
|
||||
await expect(page.getByLabel('action-Action-Delete-destroy-general-details')).toBeVisible();
|
||||
|
||||
// create print
|
||||
await createAction(page, 'Print');
|
||||
|
@ -48,13 +48,13 @@ test.describe('association form block', () => {
|
||||
test('association table block add new ', async ({ page, mockPage, mockRecord }) => {
|
||||
await mockPage(T3979).goto();
|
||||
await mockRecord('general');
|
||||
await expect(await page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
||||
// 1. 打开关系字段弹窗
|
||||
await page.getByLabel('block-item-CardItem-general-').locator('a').click();
|
||||
await page.getByLabel('block-item-CardItem-roles-').click();
|
||||
|
||||
// 2. 提交后,Table 会显示新增的数据
|
||||
await page.getByLabel('action-Action-Add new-create-roles-table-').click();
|
||||
await page.getByLabel('action-Action-Add new-create-roles-table').click();
|
||||
|
||||
// 3. 区块数据表为关系字段的区块
|
||||
await page
|
||||
@ -64,6 +64,6 @@ test.describe('association form block', () => {
|
||||
|
||||
await page.getByRole('menuitem', { name: 'form Form' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Current collection' }).click();
|
||||
await expect(await page.getByLabel('block-item-CardItem-roles-form')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CardItem-roles-form')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -17,7 +17,15 @@ import {
|
||||
test,
|
||||
} from '@nocobase/test/e2e';
|
||||
import { oneEmptyTableWithUsers } from '../../../details-multi/__e2e__/templatesOfBug';
|
||||
import { T2174, T3871, oneFormAndOneTableWithUsers, oneTableWithNestPopups } from './templatesOfBug';
|
||||
import {
|
||||
T2174,
|
||||
T3871,
|
||||
currentPopupRecordInPopupThatOpenedByAssociationField,
|
||||
oneFormAndOneTableWithUsers,
|
||||
oneTableWithNestPopups,
|
||||
parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsAPopup,
|
||||
parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsASubpageToo,
|
||||
} from './templatesOfBug';
|
||||
|
||||
test.describe('set default value', () => {
|
||||
test('basic fields', async ({ page, mockPage }) => {
|
||||
@ -203,7 +211,6 @@ test.describe('set default value', () => {
|
||||
// https://nocobase.height.app/T-4028/description
|
||||
// 刷新页面后,默认值应该依然存在
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-general-Add record')
|
||||
.getByRole('button', { name: 'Add new' })
|
||||
@ -363,11 +370,51 @@ test.describe('set default value', () => {
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Current popup record in popup that opened by association field', async ({ mockPage, page }) => {
|
||||
await mockPage(currentPopupRecordInPopupThatOpenedByAssociationField).goto();
|
||||
|
||||
await page.getByText('Member').click();
|
||||
|
||||
// Current popup record in the first popup
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Member');
|
||||
|
||||
// Current popup record and Parent popup record in the second popup
|
||||
await page.getByLabel('action-Action-Edit-update-').click();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-Edit record')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Current popup record')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('Member');
|
||||
await expect(
|
||||
page.getByLabel('block-item-CollectionField-users-form-users.username-Parent popup record').getByRole('textbox'),
|
||||
).toHaveValue('Member');
|
||||
});
|
||||
|
||||
test('Parent popup record', async ({ page, mockPage }) => {
|
||||
await mockPage(oneTableWithNestPopups).goto();
|
||||
|
||||
// 1. 表单字段默认值中使用 `Parent popup record`
|
||||
await page.getByLabel('action-Action.Link-View').click();
|
||||
|
||||
// 在第一级弹窗中,不应该包含 Parent popup record 变量
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('block-item-CardItem-users-')
|
||||
.hover();
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users')
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible();
|
||||
|
||||
// 关闭数据范围设置弹窗
|
||||
await page.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
|
||||
await page.getByLabel('action-Action.Link-View in popup').click();
|
||||
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover();
|
||||
@ -462,11 +509,273 @@ test.describe('set default value', () => {
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
});
|
||||
|
||||
test('Parent popup record in sub page. The first level is a subpage, and the second level is a popup', async ({
|
||||
page,
|
||||
mockPage,
|
||||
}) => {
|
||||
await mockPage(parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsAPopup).goto();
|
||||
|
||||
// 1. 表单字段默认值中使用 `Parent popup record`
|
||||
await page.getByLabel('action-Action.Link-View').click();
|
||||
|
||||
// 在第一级弹窗中,不应该包含 Parent popup record 变量
|
||||
await page.getByLabel('block-item-CardItem-users-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible();
|
||||
|
||||
// 关闭数据范围设置弹窗
|
||||
await page.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
|
||||
await page.getByLabel('action-Action.Link-View in popup').click();
|
||||
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Users' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
await page.getByLabel('block-item-CollectionField-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
// 2. 表单联动规则中使用 `Parent popup record`
|
||||
// 创建 Username 字段
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Username' }).click();
|
||||
// 设置联动规则
|
||||
await page.getByLabel('block-item-CardItem-users-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||
await page.getByText('Add property').click();
|
||||
await page.getByTestId('select-linkage-property-field').click();
|
||||
await page.getByTitle('Username').click();
|
||||
await page.getByTestId('select-linkage-action-field').click();
|
||||
await page.getByRole('option', { name: 'Value', exact: true }).click();
|
||||
await page.getByTestId('select-linkage-value-type').click();
|
||||
await page.getByTitle('Expression').click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Username' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
// 需正确显示变量的值
|
||||
await expect(
|
||||
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// 3. Table 数据选择器中使用 `Parent popup record`
|
||||
// 创建 Table 区块
|
||||
await page.getByLabel('schema-initializer-Grid-popup').nth(1).hover();
|
||||
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Users' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
// 显示 Nickname 字段
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('schema-initializer-TableV2-')
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
// 设置数据范围(使用 `Parent popup record` 变量)
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('block-item-CardItem-users-table')
|
||||
.hover();
|
||||
await page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users')
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
// 数据需显示正确
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('block-item-CardItem-users-table')
|
||||
.getByRole('button', { name: 'Super Admin' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-users-View record')
|
||||
.getByLabel('block-item-CardItem-users-table')
|
||||
.getByRole('button', { name: 'Super Admin' }),
|
||||
).toBeVisible();
|
||||
|
||||
// 4. 退出二级弹窗,在第一级弹窗中点击 Add new 按钮
|
||||
await page.goBack();
|
||||
await page.getByLabel('action-Action-Add new-create-').click();
|
||||
|
||||
// 5. 在新增表单中使用 `Parent popup record`
|
||||
await page.getByLabel('block-item-CollectionField-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
});
|
||||
|
||||
test('Parent popup record in sub page. The first level is a subpage, and the second level is a subpage too', async ({
|
||||
page,
|
||||
mockPage,
|
||||
}) => {
|
||||
await mockPage(parentPopupRecordInSubPageTheFirstLevelIsASubpageAndTheSecondLevelIsASubpageToo).goto();
|
||||
|
||||
// 1. 表单字段默认值中使用 `Parent popup record`
|
||||
await page.getByLabel('action-Action.Link-View').click();
|
||||
|
||||
// 在第一级弹窗中,不应该包含 Parent popup record 变量
|
||||
await page.getByLabel('block-item-CardItem-users-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' })).not.toBeVisible();
|
||||
|
||||
// 关闭数据范围设置弹窗
|
||||
await page.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
|
||||
await page.getByLabel('action-Action.Link-View in popup').click();
|
||||
await page.getByLabel('schema-initializer-Grid-popup').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form (Add new) right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Users' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
await page.getByLabel('block-item-CollectionField-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
// 2. 表单联动规则中使用 `Parent popup record`
|
||||
// 创建 Username 字段
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Username' }).click();
|
||||
// 设置联动规则
|
||||
await page.getByLabel('block-item-CardItem-users-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:createForm-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Linkage rules' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await page.getByRole('button', { name: 'plus Add linkage rule' }).click();
|
||||
await page.getByText('Add property').click();
|
||||
await page.getByTestId('select-linkage-property-field').click();
|
||||
await page.getByTitle('Username').click();
|
||||
await page.getByTestId('select-linkage-action-field').click();
|
||||
await page.getByRole('option', { name: 'Value', exact: true }).click();
|
||||
await page.getByTestId('select-linkage-value-type').click();
|
||||
await page.getByTitle('Expression').click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Username' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
// 需正确显示变量的值
|
||||
await expect(
|
||||
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// 3. Table 数据选择器中使用 `Parent popup record`
|
||||
// 创建 Table 区块
|
||||
await page.getByLabel('schema-initializer-Grid-popup').hover();
|
||||
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Other records right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Users' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
// 显示 Nickname 字段
|
||||
await page.getByLabel('schema-initializer-TableV2-').hover();
|
||||
await page.getByRole('menuitem', { name: 'Nickname' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
// 设置数据范围(使用 `Parent popup record` 变量)
|
||||
await page.getByLabel('block-item-CardItem-users-table').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:table-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set the data scope' }).click();
|
||||
await page.getByText('Add condition', { exact: true }).click();
|
||||
await page.getByTestId('select-filter-field').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
// 数据需显示正确
|
||||
await expect(
|
||||
page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
page.getByLabel('block-item-CardItem-users-table').getByRole('button', { name: 'Super Admin' }),
|
||||
).toBeVisible();
|
||||
|
||||
// 4. 退出二级弹窗,在第一级弹窗中点击 Add new 按钮
|
||||
await page.goBack();
|
||||
await page.getByLabel('action-Action-Add new-create-').click();
|
||||
|
||||
// 5. 在新增表单中使用 `Parent popup record`
|
||||
await page.getByLabel('block-item-CollectionField-').hover();
|
||||
await page.getByLabel('designer-schema-settings-CollectionField-fieldSettings:FormItem-users-users.').hover();
|
||||
await page.getByRole('menuitem', { name: 'Set default value' }).click();
|
||||
await page.getByLabel('variable-button').click();
|
||||
await expect(page.getByRole('menuitemcheckbox', { name: 'Current popup record right' })).not.toBeVisible();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Parent popup record right' }).click();
|
||||
await page.getByRole('menuitemcheckbox', { name: 'Nickname' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('block-item-CollectionField-').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('actions schema settings', () => {
|
||||
|
@ -48,7 +48,6 @@ test.describe('creation form block schema settings', () => {
|
||||
|
||||
// 刷新页面后,显示的应该依然是上次设置的值
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await runExpect();
|
||||
});
|
||||
|
||||
@ -68,7 +67,6 @@ test.describe('creation form block schema settings', () => {
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible();
|
||||
@ -82,7 +80,6 @@ test.describe('creation form block schema settings', () => {
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible();
|
||||
@ -115,7 +112,6 @@ test.describe('creation form block schema settings', () => {
|
||||
|
||||
// 刷新页面后,区块依然是被删除状态
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
||||
});
|
||||
|
||||
|
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();
|
||||
|
||||
// 1.打开弹窗
|
||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').hover();
|
||||
await page.getByRole('button', { name: '2', exact: true }).getByText('2').click();
|
||||
|
||||
// 2.创建一个编辑表单区块
|
||||
|
@ -50,7 +50,6 @@ test.describe('edit form block schema settings', () => {
|
||||
|
||||
// 刷新页面后,显示的应该依然是上次设置的值
|
||||
await page.reload();
|
||||
await page.getByText('Edit', { exact: true }).click();
|
||||
await runExpect();
|
||||
});
|
||||
|
||||
@ -81,7 +80,6 @@ test.describe('edit form block schema settings', () => {
|
||||
|
||||
// 刷新页面后,设置的值应该依然存在
|
||||
await page.reload();
|
||||
await page.getByText('Edit', { exact: true }).click();
|
||||
await clickOption(page, 'Linkage rules');
|
||||
await runExpect();
|
||||
});
|
||||
@ -104,7 +102,6 @@ test.describe('edit form block schema settings', () => {
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
await page.getByText('Edit', { exact: true }).click();
|
||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).not.toBeVisible();
|
||||
@ -117,7 +114,6 @@ test.describe('edit form block schema settings', () => {
|
||||
|
||||
// 刷新页面
|
||||
await page.reload();
|
||||
await page.getByText('Edit', { exact: true }).click();
|
||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.Designer-general').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'Save as block template' })).toBeVisible();
|
||||
@ -152,30 +148,28 @@ test.describe('edit form block schema settings', () => {
|
||||
|
||||
// 刷新页面后,区块依然是被删除状态
|
||||
await page.reload();
|
||||
await page.getByText('Edit', { exact: true }).click();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
||||
});
|
||||
// https://nocobase.height.app/T-3825
|
||||
test('Unsaved changes warning display', async ({ page, mockPage, mockRecord }) => {
|
||||
await mockPage(T3825).goto();
|
||||
await mockRecord('general', { number: 9, formula: 10 });
|
||||
await expect(await page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-')).toBeVisible();
|
||||
//没有改动时不显示提示
|
||||
await page.getByLabel('action-Action.Link-Edit-').click();
|
||||
await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click();
|
||||
await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click();
|
||||
await expect(await page.getByLabel('action-Action-Add new-create-')).toBeVisible();
|
||||
await expect(page.getByLabel('action-Action-Add new-create-')).toBeVisible();
|
||||
//有改动时显示提示
|
||||
await page.getByLabel('action-Action.Link-Edit-').click();
|
||||
await page.getByLabel('action-Action.Link-Edit record-update-general-table-').click();
|
||||
await page.getByRole('spinbutton').fill('');
|
||||
await page.getByRole('spinbutton').fill('10');
|
||||
await expect(
|
||||
await page
|
||||
page
|
||||
.getByLabel('block-item-CollectionField-general-form-general.formula-formula')
|
||||
.locator('.nb-read-pretty-input-number')
|
||||
.innerText(),
|
||||
).toBe('11');
|
||||
.locator('.nb-read-pretty-input-number'),
|
||||
).toHaveText('11');
|
||||
await page.getByLabel('drawer-Action.Container-general-Edit record-mask').click();
|
||||
await expect(await page.getByText('Unsaved changes')).toBeVisible();
|
||||
await expect(page.getByText('Unsaved changes')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -426,7 +426,7 @@ export const T3825: PageConfig = {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: '{{ t("Edit") }}',
|
||||
title: '{{ t("Edit record") }}',
|
||||
'x-action': 'update',
|
||||
'x-toolbar': 'ActionSchemaToolbar',
|
||||
'x-settings': 'actionSettings:edit',
|
||||
|
@ -16,7 +16,6 @@ import { useAPIClient } from '../../../../api-client';
|
||||
import { CompatibleSchemaInitializer } from '../../../../application/schema-initializer/CompatibleSchemaInitializer';
|
||||
import { SchemaInitializerActionModal } from '../../../../application/schema-initializer/components/SchemaInitializerActionModal';
|
||||
import { SchemaInitializerItem } from '../../../../application/schema-initializer/components/SchemaInitializerItem';
|
||||
import { useSchemaInitializer } from '../../../../application/schema-initializer/context';
|
||||
import { useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { SelectWithTitle } from '../../../../common/SelectWithTitle';
|
||||
import { useDataBlockProps } from '../../../../data-source';
|
||||
|
@ -17,7 +17,13 @@ import {
|
||||
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
|
||||
test,
|
||||
} from '@nocobase/test/e2e';
|
||||
import { T3843, oneTableWithColumnFixed, oneTableWithUpdateRecord } from './templatesOfBug';
|
||||
import {
|
||||
T3843,
|
||||
oneTableWithColumnFixed,
|
||||
oneTableWithUpdateRecord,
|
||||
testingOfOpenModeForAddChild,
|
||||
testingWithPageMode,
|
||||
} from './templatesOfBug';
|
||||
|
||||
const addSomeCustomActions = async (page: Page) => {
|
||||
// 先删除掉之前的 actions
|
||||
@ -50,7 +56,7 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
|
||||
};
|
||||
|
||||
test('supported options', async ({ page, mockPage, mockRecord }) => {
|
||||
test('supported options', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await expectSettingsMenu({
|
||||
@ -60,7 +66,7 @@ test.describe('actions schema settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('edit button', async ({ page, mockPage, mockRecord }) => {
|
||||
test('edit button', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await showMenu(page);
|
||||
@ -72,7 +78,7 @@ test.describe('actions schema settings', () => {
|
||||
await expect(page.getByRole('button', { name: '1234' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('open mode', async ({ page, mockPage, mockRecord }) => {
|
||||
test('open mode', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
await showMenu(page);
|
||||
|
||||
@ -85,9 +91,45 @@ test.describe('actions schema settings', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
await expect(page.getByTestId('modal-Action.Container-general-Add record')).toBeVisible();
|
||||
await page.getByLabel('modal-Action.Container-general-Add record-mask').click();
|
||||
|
||||
// 切换为 page
|
||||
await page.getByLabel('action-Action-Add new-create-').hover();
|
||||
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Open mode Dialog' }).click();
|
||||
await page.getByRole('option', { name: 'Page' }).click();
|
||||
|
||||
// 点击按钮后会跳转到一个页面
|
||||
await page.getByLabel('action-Action-Add new-create-').click();
|
||||
expect(page.url()).toContain('/subpages/');
|
||||
|
||||
// 配置出一个表单
|
||||
await page.getByLabel('schema-initializer-Grid-popup').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Current collection' }).click();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-form:').hover();
|
||||
await page.getByRole('menuitem', { name: 'Single select' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
|
||||
await page.getByLabel('schema-initializer-ActionBar-').hover();
|
||||
await page.getByRole('menuitem', { name: 'Submit' }).click();
|
||||
|
||||
// 创建一条数据后返回,列表中应该有这条数据
|
||||
await page.getByTestId('select-single').click();
|
||||
await page.getByRole('option', { name: 'option3' }).click();
|
||||
|
||||
await page.getByLabel('action-Action-Submit-submit-').click();
|
||||
|
||||
await page.goBack();
|
||||
|
||||
await page.getByLabel('schema-initializer-TableV2-').hover();
|
||||
await page.getByRole('menuitem', { name: 'Single select' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await expect(page.getByRole('button', { name: 'option3' })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('popup size', async ({ page, mockPage, mockRecord }) => {
|
||||
test('popup size', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await showMenu(page);
|
||||
@ -116,7 +158,7 @@ test.describe('actions schema settings', () => {
|
||||
expect(drawerWidth2).toBeGreaterThanOrEqual(800);
|
||||
});
|
||||
|
||||
test('delete', async ({ page, mockPage, mockRecord }) => {
|
||||
test('delete', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await showMenu(page);
|
||||
@ -149,7 +191,7 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByLabel('designer-schema-settings-Filter.Action-Filter.Action.Designer-general').hover();
|
||||
};
|
||||
|
||||
test('supported options', async ({ page, mockPage, mockRecord }) => {
|
||||
test('supported options', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await expectSettingsMenu({
|
||||
@ -316,6 +358,390 @@ test.describe('actions schema settings', () => {
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
test('open mode: page', async ({ page, mockPage }) => {
|
||||
await mockPage(testingWithPageMode).goto();
|
||||
|
||||
// 打开弹窗
|
||||
await page.getByLabel('action-Action.Link-View').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-users-details')
|
||||
.getByLabel('block-item-CollectionField-users-details-users.nickname-Nickname'),
|
||||
).toHaveText('Nickname:Super Admin');
|
||||
|
||||
// 关系区块
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-roles-details')
|
||||
.getByLabel('block-item-CollectionField-roles-details-roles.name-Role UID'),
|
||||
).toHaveText('Role UID:admin');
|
||||
|
||||
// 使用变量 `Current role` 设置默认值
|
||||
await expect(page.getByLabel('block-item-CardItem-roles-form').getByRole('textbox')).toHaveValue('root');
|
||||
|
||||
// 使用变量 `Current popup record` 设置默认值
|
||||
await expect(page.getByLabel('block-item-CardItem-users-form').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
// 打开嵌套弹窗
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Admin',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('admin');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 关闭嵌套弹窗后,再点击不同的行打开嵌套弹窗,应该显示不同的数据
|
||||
await page.getByLabel('drawer-Action.Container-roles-View record-mask').click();
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-member').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Member',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('member');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 刷新页面后,弹窗中的内容不变
|
||||
await page.reload();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Member',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('member');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 关闭弹窗,然后将 Open mode 调整为 page
|
||||
await page.getByLabel('drawer-Action.Container-roles-View record-mask').click();
|
||||
await page.locator('.ant-drawer-mask').click();
|
||||
|
||||
await page.getByLabel('action-Action.Link-View').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:view-users').hover();
|
||||
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
|
||||
await page.getByRole('option', { name: 'Page' }).click();
|
||||
|
||||
// 跳转到子页面后,其内容应该和弹窗中的内容一致
|
||||
await page.getByLabel('action-Action.Link-View').click();
|
||||
expect(page.url()).toContain('/subpages');
|
||||
|
||||
// 详情区块
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-users-details')
|
||||
.getByLabel('block-item-CollectionField-users-details-users.nickname-Nickname'),
|
||||
).toHaveText('Nickname:Super Admin');
|
||||
|
||||
// 关系区块
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-roles-details')
|
||||
.getByLabel('block-item-CollectionField-roles-details-roles.name-Role UID'),
|
||||
).toHaveText('Role UID:admin');
|
||||
|
||||
// 使用变量 `Current role` 设置默认值
|
||||
await expect(page.getByLabel('block-item-CardItem-roles-form').getByRole('textbox')).toHaveValue('root');
|
||||
|
||||
// 使用变量 `Current popup record` 设置默认值
|
||||
await expect(page.getByLabel('block-item-CardItem-users-form').getByRole('textbox')).toHaveValue('Super Admin');
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// 打开子页面中的弹窗
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-member').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Member',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('member');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 关闭弹窗后,重新选择一条不同的数据打开,应该显示不同的数据
|
||||
await page.getByLabel('drawer-Action.Container-roles-View record-mask').click();
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Admin',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('admin');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 刷新页面后,弹窗中的内容不变
|
||||
await page.reload();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Admin',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-roles-table')
|
||||
.getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('admin');
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('drawer-Action.Container-roles-View record')
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
|
||||
// -----------------------------------------------------------------------------------
|
||||
|
||||
// 关闭弹窗后,将子页面中的 Open mode 调整为 page
|
||||
await page.goBack();
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').hover();
|
||||
await page
|
||||
.getByRole('button', { name: 'designer-schema-settings-Action.Link-actionSettings:view-roles' })
|
||||
.hover();
|
||||
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
|
||||
await page.getByRole('option', { name: 'Page' }).click();
|
||||
|
||||
// 点击按钮跳转到子页面
|
||||
await page.getByLabel('action-Action.Link-View role-view-roles-table-admin').click();
|
||||
|
||||
// 详情区块
|
||||
await expect(page.getByLabel('block-item-CollectionField-roles-details-roles.title-Role name')).toHaveText(
|
||||
'Role name:Admin',
|
||||
);
|
||||
|
||||
// 使用变量 `Parent popup record` 设置数据范围
|
||||
await expect(
|
||||
page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Admin', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Member', exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByLabel('block-item-CardItem-roles-table').getByRole('button', { name: 'Root', exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
// 使用变量 `Current popup record` 和 `Parent popup record` 设置默认值
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('admin');
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('block-item-CardItem-users-form')
|
||||
.getByLabel('block-item-CollectionField-users-form-users.username-Username')
|
||||
.getByRole('textbox'),
|
||||
).toHaveValue('nocobase');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('popup', () => {
|
||||
@ -459,7 +885,7 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-tree').hover();
|
||||
};
|
||||
|
||||
test('supported options', async ({ page, mockPage, mockRecord }) => {
|
||||
test('supported options', async ({ page, mockPage }) => {
|
||||
const nocoPage = await mockPage(oneEmptyTableWithTreeCollection).waitForInit();
|
||||
await nocoPage.goto();
|
||||
await page.getByLabel('block-item-CardItem-treeCollection-table').hover();
|
||||
@ -511,6 +937,35 @@ test.describe('actions schema settings', () => {
|
||||
.getByText('1', { exact: true }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('open mode', async ({ page, mockPage }) => {
|
||||
const nocoPage = await mockPage(testingOfOpenModeForAddChild).waitForInit();
|
||||
await nocoPage.goto();
|
||||
|
||||
// add a record
|
||||
await page.getByLabel('action-Action-Add new-create-').click();
|
||||
await page.getByLabel('action-Action-Submit-submit-').click();
|
||||
|
||||
// open popup with drawer mode
|
||||
await page.getByLabel('action-Action.Link-Add child-').click();
|
||||
await expect(page.getByTestId('select-object-single')).toHaveText('1');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('select-object-single')).toHaveText('1');
|
||||
|
||||
await page.goBack();
|
||||
await page.getByLabel('action-Action.Link-Add child-').hover();
|
||||
await page.getByLabel('designer-schema-settings-Action.Link-actionSettings:addChild-treeCollection').hover();
|
||||
await page.getByRole('menuitem', { name: 'Open mode Drawer' }).click();
|
||||
await page.getByRole('option', { name: 'Page' }).click();
|
||||
|
||||
// open popup with page mode
|
||||
await page.getByLabel('action-Action.Link-Add child-').click();
|
||||
await expect(page.getByTestId('select-object-single')).toHaveText('1');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByTestId('select-object-single')).toHaveText('1');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('add record', () => {
|
||||
@ -519,7 +974,7 @@ test.describe('actions schema settings', () => {
|
||||
await page.getByRole('button', { name: 'designer-schema-settings-Action-Action.Designer-general' }).hover();
|
||||
};
|
||||
|
||||
test('supported options', async ({ page, mockPage, mockRecord }) => {
|
||||
test('supported options', async ({ page, mockPage }) => {
|
||||
await mockPage(oneEmptyTableBlockWithActions).goto();
|
||||
|
||||
await expectSettingsMenu({
|
||||
|
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 { useAPIClient } from '../../../../api-client';
|
||||
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
|
||||
import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils/createSwitchSettingsItem';
|
||||
import { useTableBlockContext } from '../../../../block-provider';
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../../collection-manager';
|
||||
import { FilterBlockType } from '../../../../filter-provider/utils';
|
||||
@ -20,10 +21,9 @@ import { SchemaSettingsBlockTitleItem } from '../../../../schema-settings/Schema
|
||||
import { SchemaSettingsConnectDataBlocks } from '../../../../schema-settings/SchemaSettingsConnectDataBlocks';
|
||||
import { SchemaSettingsSortField } from '../../../../schema-settings/SchemaSettingsSortField';
|
||||
import { SchemaSettingsTemplate } from '../../../../schema-settings/SchemaSettingsTemplate';
|
||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||
import { setDefaultSortingRulesSchemaSettingsItem } from '../../../../schema-settings/setDefaultSortingRulesSchemaSettingsItem';
|
||||
import { setTheDataScopeSchemaSettingsItem } from '../../../../schema-settings/setTheDataScopeSchemaSettingsItem';
|
||||
import { createSwitchSettingsItem } from '../../../../application/schema-settings/utils';
|
||||
import { setDataLoadingModeSettingsItem } from '../details-multi/setDataLoadingModeSettingsItem';
|
||||
|
||||
export const tableBlockSettings = new SchemaSettings({
|
||||
name: 'blockSettings:table',
|
||||
|
@ -171,7 +171,7 @@ test.describe('where filter block can be added', () => {
|
||||
await connectToOtherBlock('Users #o1nq');
|
||||
|
||||
await page.getByLabel('block-item-CardItem-users-filter-form').getByRole('textbox').fill(usersRecords[0].nickname);
|
||||
await page.getByLabel('action-Action-Filter-submit-users-filter-form-').click({ position: { x: 10, y: 10 } });
|
||||
await page.getByLabel('action-Action-Filter-submit-users-filter-form').click({ position: { x: 10, y: 10 } });
|
||||
await page.waitForLoadState('networkidle');
|
||||
for (const record of usersRecords) {
|
||||
await expect(page.getByLabel('block-item-CardItem-users-details').getByText(record.nickname)).toBeVisible({
|
||||
|
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', () => {
|
||||
async function showSettings(page: Page) {
|
||||
async function showSettingsOfTab(page: Page) {
|
||||
await page.getByText('Unnamed').hover();
|
||||
await page.getByRole('tab').getByLabel('designer-schema-settings-Page').hover();
|
||||
}
|
||||
@ -87,7 +87,7 @@ test.describe('tabs schema settings', () => {
|
||||
await mockPage().goto();
|
||||
await enablePageTabs(page);
|
||||
|
||||
await showSettings(page);
|
||||
await showSettingsOfTab(page);
|
||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').click();
|
||||
await page.getByLabel('block-item-Input-Tab name').getByRole('textbox').fill('new name of page tab');
|
||||
@ -103,7 +103,7 @@ test.describe('tabs schema settings', () => {
|
||||
await mockPage().goto();
|
||||
await enablePageTabs(page);
|
||||
|
||||
await showSettings(page);
|
||||
await showSettingsOfTab(page);
|
||||
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
|
||||
|
@ -7,3 +7,135 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
export const pageTabRouting = {
|
||||
pageSchema: {
|
||||
'x-uid': 'i7o68vu98u8',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Page',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
'x-component-props': {
|
||||
enablePageTabs: true,
|
||||
},
|
||||
properties: {
|
||||
u4earq3d9go: {
|
||||
'x-uid': 'xbx6zg90ij2',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
title: 'tab1',
|
||||
properties: {
|
||||
nq41b5sodws: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
properties: {
|
||||
mhiac303xfd: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
properties: {
|
||||
feimcvzb7rs: {
|
||||
'x-uid': 'maurznopx6g',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-settings': 'blockSettings:markdown',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-decorator-props': {
|
||||
name: 'markdown',
|
||||
},
|
||||
'x-component': 'Markdown.Void',
|
||||
'x-editable': false,
|
||||
'x-component-props': {
|
||||
content: 'This is tab1.',
|
||||
},
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'im4ufr0vu12',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'nt3axo5te3r',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
bbch3c9b5jl: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
title: 'tab2',
|
||||
'x-component': 'Grid',
|
||||
'x-initializer': 'page:addBlock',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
properties: {
|
||||
'8veis5l6j8x': {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
properties: {
|
||||
ayw9hqnz30u: {
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
properties: {
|
||||
njodwctznc4: {
|
||||
'x-uid': 'ox614ghc544',
|
||||
_isJSONSchemaObject: true,
|
||||
version: '2.0',
|
||||
type: 'void',
|
||||
'x-settings': 'blockSettings:markdown',
|
||||
'x-decorator': 'CardItem',
|
||||
'x-decorator-props': {
|
||||
name: 'markdown',
|
||||
},
|
||||
'x-component': 'Markdown.Void',
|
||||
'x-editable': false,
|
||||
'x-component-props': {
|
||||
content: 'This is tab2.',
|
||||
},
|
||||
'x-app-version': '1.2.3-alpha',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ca5bt5ygrkp',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'ranzuquuert',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
'x-uid': 'qhjdmy9nk6q',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
},
|
||||
'x-async': true,
|
||||
'x-index': 1,
|
||||
},
|
||||
keepUid: true,
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test123');
|
||||
await page.getByLabel('action-Action-Submit-general-table').click();
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test123')).toBeVisible();
|
||||
|
||||
@ -54,7 +54,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test7');
|
||||
await page.getByLabel('action-Action-Submit-general-table').click();
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test7')).toBeVisible();
|
||||
|
||||
@ -83,7 +83,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test8');
|
||||
await page.getByLabel('action-Action-Submit-general-table-0').click();
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test8')).toBeVisible();
|
||||
|
||||
@ -140,7 +140,7 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test1');
|
||||
await page.getByLabel('action-Action-Submit-general-table').click();
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test1')).toBeVisible();
|
||||
|
||||
@ -171,7 +171,73 @@ test.describe('where to open a popup and what can be added to it', () => {
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test8');
|
||||
await page.getByLabel('action-Action-Submit-general-details').click();
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test8')).toBeVisible();
|
||||
|
||||
// add blocks
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Details' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Current record' }).click();
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'form Form (Edit)' }).first().click();
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Markdown' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
|
||||
await expect(page.getByLabel('block-item-CardItem-general-details')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-CardItem-general-form')).toBeVisible();
|
||||
await expect(page.getByLabel('block-item-Markdown.Void-')).toBeVisible();
|
||||
|
||||
// add relationship blocks
|
||||
// 下拉列表中,可选择以下区块进行创建
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await expect(page.getByRole('menuitem', { name: 'table Details right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'form Form (Edit)' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'form Form (Add new) right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'form Form (Add new) right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'table Table right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'ordered-list List right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'ordered-list Grid Card right' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Calendar' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Gantt' })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: 'Kanban' })).toBeVisible();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'Details' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Many to one' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await expect(page.getByLabel('block-item-CardItem-users-')).toBeVisible();
|
||||
|
||||
await page.getByLabel('schema-initializer-Grid-popup:common:addBlock-general').hover();
|
||||
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'Associated records' }).hover();
|
||||
await page.getByRole('menuitem', { name: 'One to many' }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible();
|
||||
// 屏幕上没有显示错误提示
|
||||
await expect(page.locator('.ant-notification-notice').first()).toBeHidden({ timeout: 1000 });
|
||||
});
|
||||
|
||||
test('association link after reload page', async ({ page, mockPage, mockRecord }) => {
|
||||
const nocoPage = await mockPage(oneDetailBlockWithM2oFieldToGeneral).waitForInit();
|
||||
const record = await mockRecord('targetToGeneral');
|
||||
await nocoPage.goto();
|
||||
|
||||
// open dialog
|
||||
await page
|
||||
.getByLabel('block-item-CollectionField-targetToGeneral-details-targetToGeneral.toGeneral-toGeneral')
|
||||
.getByText(record.id, { exact: true })
|
||||
.click();
|
||||
|
||||
await page.reload();
|
||||
|
||||
// add a tab
|
||||
await page.getByLabel('schema-initializer-Tabs-').click();
|
||||
await page.getByRole('textbox').click();
|
||||
await page.getByRole('textbox').fill('test8');
|
||||
await page.getByLabel('action-Action-Submit-general').click();
|
||||
|
||||
await expect(page.getByText('test8')).toBeVisible();
|
||||
|
||||
|
@ -31,6 +31,9 @@ test.describe('tabs schema settings', () => {
|
||||
await page.goto(commonPageUrl);
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
|
||||
// There will be a prompt at the top, wait for it to disappear
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await showSettings(page);
|
||||
await page.getByRole('menuitem', { name: 'Edit', exact: true }).click();
|
||||
await page.mouse.move(300, 0);
|
||||
@ -48,6 +51,9 @@ test.describe('tabs schema settings', () => {
|
||||
await page.goto(commonPageUrl);
|
||||
await page.getByRole('button', { name: 'Add new' }).click();
|
||||
|
||||
// There will be a prompt at the top, wait for it to disappear
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await showSettings(page);
|
||||
await page.getByRole('menuitem', { name: 'Delete', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
|
@ -24,8 +24,10 @@ import { RemoteDocumentTitlePlugin } from '../document-title';
|
||||
import { PinnedListPlugin } from '../plugin-manager';
|
||||
import { PMPlugin } from '../pm';
|
||||
import { AdminLayoutPlugin, RouteSchemaComponent } from '../route-switch';
|
||||
import { AntdSchemaComponentPlugin, SchemaComponentPlugin } from '../schema-component';
|
||||
import { AntdSchemaComponentPlugin, PageTabs, SchemaComponentPlugin } from '../schema-component';
|
||||
import { ErrorFallback } from '../schema-component/antd/error-fallback';
|
||||
import { PagePopups } from '../schema-component/antd/page/PagePopups';
|
||||
import { SubPage } from '../schema-component/antd/page/SubPages';
|
||||
import { AssociationFilterPlugin, SchemaInitializerPlugin } from '../schema-initializer';
|
||||
import { SchemaSettingsPlugin } from '../schema-settings';
|
||||
import { BlockTemplateDetails, BlockTemplatePage } from '../schema-templates';
|
||||
@ -302,6 +304,22 @@ export class NocoBaseBuildInPlugin extends Plugin {
|
||||
path: '/admin/:name',
|
||||
Component: 'AdminDynamicPage',
|
||||
});
|
||||
this.router.add('admin.page.tab', {
|
||||
path: '/admin/:name/tabs/:tabUid',
|
||||
Component: PageTabs as any,
|
||||
});
|
||||
this.router.add('admin.page.popup', {
|
||||
path: '/admin/:name/popups/*',
|
||||
Component: PagePopups,
|
||||
});
|
||||
this.router.add('admin.page.tab.popup', {
|
||||
path: '/admin/:name/tabs/:tabUid/popups/*',
|
||||
Component: PagePopups,
|
||||
});
|
||||
this.router.add('admin.subPage', {
|
||||
path: '/admin/subpages/*',
|
||||
Component: SubPage,
|
||||
});
|
||||
}
|
||||
|
||||
addComponents() {
|
||||
|
@ -10,12 +10,11 @@
|
||||
import { PageHeader } from '@ant-design/pro-layout';
|
||||
import { css } from '@emotion/css';
|
||||
import { Layout, Menu, Result } from 'antd';
|
||||
import _, { get } from 'lodash';
|
||||
import React, { createContext, useCallback, useMemo } from 'react';
|
||||
import { Navigate, Outlet, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useStyles } from './style';
|
||||
import { ADMIN_SETTINGS_PATH, PluginSettingsPageType, useApp } from '../application';
|
||||
import { useCompile } from '../schema-component';
|
||||
import { useStyles } from './style';
|
||||
|
||||
export const SettingsCenterContext = createContext<any>({});
|
||||
SettingsCenterContext.displayName = 'SettingsCenterContext';
|
||||
|
@ -11,8 +11,8 @@ import { css } from '@emotion/css';
|
||||
import { useSessionStorageState } from 'ahooks';
|
||||
import { App, ConfigProvider, Divider, Layout } from 'antd';
|
||||
import { createGlobalStyle } from 'antd-style';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, Outlet, useLocation, useMatch, useNavigate, useParams } from 'react-router-dom';
|
||||
import React, { FC, createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Link, Outlet, useMatch, useParams } from 'react-router-dom';
|
||||
import {
|
||||
ACLRolesCheckProvider,
|
||||
CurrentAppInfoProvider,
|
||||
@ -33,11 +33,12 @@ import {
|
||||
useSystemSettings,
|
||||
useToken,
|
||||
} from '../../../';
|
||||
import { useLocationNoUpdate, useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||
import { Plugin } from '../../../application/Plugin';
|
||||
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
||||
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||
import { Help } from '../../../user/Help';
|
||||
import { VariablesProvider } from '../../../variables';
|
||||
import { useMenuTranslation } from '../../../schema-component/antd/menu/locale';
|
||||
|
||||
const filterByACL = (schema, options) => {
|
||||
const { allowAll, allowMenuItemIds = [] } = options;
|
||||
@ -76,13 +77,13 @@ const useMenuProps = () => {
|
||||
|
||||
const MenuEditor = (props) => {
|
||||
const { notification } = App.useApp();
|
||||
const [hasNotice, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
|
||||
const [, setHasNotice] = useSessionStorageState('plugin-notice', { defaultValue: false });
|
||||
const { t } = useMenuTranslation();
|
||||
const { setTitle: _setTitle } = useDocumentTitle();
|
||||
const setTitle = useCallback((title) => _setTitle(t(title)), []);
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const params = useParams<any>();
|
||||
const location = useLocation();
|
||||
const location = useLocationNoUpdate();
|
||||
const isMatchAdmin = useMatch('/admin');
|
||||
const isMatchAdminName = useMatch('/admin/:name');
|
||||
const defaultSelectedUid = params.name;
|
||||
@ -91,12 +92,12 @@ const MenuEditor = (props) => {
|
||||
const ctx = useACLRoleContext();
|
||||
const [current, setCurrent] = useState(null);
|
||||
|
||||
const onSelect = ({ item }) => {
|
||||
const onSelect = useCallback(({ item }) => {
|
||||
const schema = item.props.schema;
|
||||
setTitle(schema.title);
|
||||
setCurrent(schema);
|
||||
navigate(`/admin/${schema['x-uid']}`);
|
||||
};
|
||||
}, []);
|
||||
const { render } = useAppSpin();
|
||||
const adminSchemaUid = useAdminSchemaUid();
|
||||
const { data, loading } = useRequest<{
|
||||
@ -154,7 +155,7 @@ const MenuEditor = (props) => {
|
||||
sideMenuRef.current.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}, [data?.data, params.name, sideMenuRef]);
|
||||
}, [data?.data, params.name, sideMenuRef, location?.pathname]);
|
||||
|
||||
const schema = useMemo(() => {
|
||||
const s = filterByACL(data?.data, ctx);
|
||||
@ -211,17 +212,16 @@ const MenuEditor = (props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const scope = useMemo(() => {
|
||||
return { useMenuProps, onSelect, sideMenuRef, defaultSelectedUid };
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return render();
|
||||
}
|
||||
return (
|
||||
<SchemaIdContext.Provider value={defaultSelectedUid}>
|
||||
<SchemaComponent
|
||||
distributed
|
||||
memoized
|
||||
scope={{ useMenuProps, onSelect, sideMenuRef, defaultSelectedUid }}
|
||||
schema={schema}
|
||||
/>
|
||||
<SchemaComponent distributed memoized scope={scope} schema={schema} />
|
||||
</SchemaIdContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -292,12 +292,7 @@ const SetThemeOfHeaderSubmenu = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const AdminSideBar = ({ sideMenuRef }) => {
|
||||
const params = useParams<any>();
|
||||
if (!params.name) return null;
|
||||
return (
|
||||
<Layout.Sider
|
||||
className={css`
|
||||
const sideClass = css`
|
||||
height: 100%;
|
||||
/* position: fixed; */
|
||||
position: relative;
|
||||
@ -311,16 +306,21 @@ const AdminSideBar = ({ sideMenuRef }) => {
|
||||
width: 200px;
|
||||
height: calc(100vh - var(--nb-header-height));
|
||||
}
|
||||
`}
|
||||
theme={'light'}
|
||||
ref={sideMenuRef}
|
||||
></Layout.Sider>
|
||||
);
|
||||
`;
|
||||
|
||||
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 params = useParams<any>();
|
||||
return <InternalAdminSideBar pageUid={params.name} sideMenuRef={sideMenuRef} />;
|
||||
};
|
||||
|
||||
export const AdminDynamicPage = () => {
|
||||
const params = useParams<{ name?: string }>();
|
||||
return <RouteSchemaComponent schema={params.name} />;
|
||||
return <RouteSchemaComponent />;
|
||||
};
|
||||
|
||||
export const InternalAdminLayout = () => {
|
||||
|
@ -11,7 +11,7 @@ import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { RemoteSchemaComponent } from '../../../';
|
||||
|
||||
export function RouteSchemaComponent(props: any) {
|
||||
const params = useParams<any>();
|
||||
export function RouteSchemaComponent() {
|
||||
const params = useParams();
|
||||
return <RemoteSchemaComponent onlyRenderProperties uid={params.name} />;
|
||||
}
|
||||
|
@ -11,13 +11,12 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea
|
||||
import { Drawer } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { ActionDrawerProps, OpenSize } from './types';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { ErrorFallback } from '../error-fallback';
|
||||
import { useStyles } from './Action.Drawer.style';
|
||||
import { useActionContext } from './hooks';
|
||||
import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer';
|
||||
import { ComposedActionDrawer } from './types';
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||
import { ErrorFallback } from '../error-fallback';
|
||||
import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types';
|
||||
|
||||
const DrawerErrorFallback: React.FC<FallbackProps> = (props) => {
|
||||
const { visible, setVisible } = useActionContext();
|
||||
|
@ -11,7 +11,7 @@ import { observer, RecursionField, useField, useFieldSchema, useForm } from '@fo
|
||||
import { isPortalInBody } from '@nocobase/utils/client';
|
||||
import { App, Button } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import { default as lodash } from 'lodash';
|
||||
import _, { default as lodash } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -28,6 +28,10 @@ import { useLocalVariables, useVariables } from '../../../variables';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useCompile, useComponent, useDesigner } from '../../hooks';
|
||||
import { useProps } from '../../hooks/useProps';
|
||||
import { PopupVisibleProvider } from '../page/PagePopups';
|
||||
import { usePagePopup } from '../page/pagePopupUtils';
|
||||
import { usePopupSettings } from '../page/PopupSettingsProvider';
|
||||
import { useNavigateTOSubPage } from '../page/SubPages';
|
||||
import ActionContainer from './Action.Container';
|
||||
import { ActionDesigner } from './Action.Designer';
|
||||
import { ActionDrawer } from './Action.Drawer';
|
||||
@ -36,11 +40,16 @@ import { ActionModal } from './Action.Modal';
|
||||
import { ActionPage } from './Action.Page';
|
||||
import useStyles from './Action.style';
|
||||
import { ActionContextProvider } from './context';
|
||||
import { useA } from './hooks';
|
||||
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||
import { ActionProps, ComposedAction } from './types';
|
||||
import { linkageAction, setInitialActionState } from './utils';
|
||||
|
||||
const useA = () => {
|
||||
return {
|
||||
async run() {},
|
||||
};
|
||||
};
|
||||
|
||||
const handleError = (err) => console.log(err);
|
||||
|
||||
export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
@ -48,7 +57,6 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
const {
|
||||
popover,
|
||||
confirm,
|
||||
openMode: om,
|
||||
containerRefKey,
|
||||
component,
|
||||
useAction = useA,
|
||||
@ -70,11 +78,13 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
const aclCtx = useACLActionParamsContext();
|
||||
const { wrapSSR, componentCls, hashId } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [formValueChanged, setFormValueChanged] = useState(false);
|
||||
const { setSubmitted: setParentSubmitted } = useActionContext();
|
||||
const Designer = useDesigner();
|
||||
const field = useField<any>();
|
||||
const { run, element, disabled: disableAction } = useAction(actionCallback);
|
||||
const { run, element, disabled: disableAction } = _.isFunction(useAction) ? useAction(actionCallback) : ({} as any);
|
||||
const fieldSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const form = useForm();
|
||||
@ -120,47 +130,12 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
});
|
||||
}, [field, linkageRules, localVariables, variables]);
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(e: React.MouseEvent, checkPortal = true) => {
|
||||
if (checkPortal && isPortalInBody(e.target as Element)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!disabled && aclCtx) {
|
||||
const onOk = () => {
|
||||
if (onClick) {
|
||||
onClick(e, () => {
|
||||
if (refreshDataBlockRequest !== false) {
|
||||
service?.refresh?.();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setVisible(true);
|
||||
run();
|
||||
}
|
||||
};
|
||||
if (confirm?.content) {
|
||||
modal.confirm({
|
||||
title: t(confirm.title, { title: actionTitle }),
|
||||
content: t(confirm.content, { title: actionTitle }),
|
||||
onOk,
|
||||
});
|
||||
} else {
|
||||
onOk();
|
||||
}
|
||||
}
|
||||
},
|
||||
[confirm, disabled, modal, onClick, run],
|
||||
);
|
||||
|
||||
const buttonStyle = useMemo(() => {
|
||||
return {
|
||||
...style,
|
||||
opacity: designable && (field?.data?.hidden || !aclCtx) && 0.1,
|
||||
};
|
||||
}, [designable, field?.data?.hidden, style]);
|
||||
}, [aclCtx, designable, field?.data?.hidden, style]);
|
||||
|
||||
const handleMouseEnter = useCallback(
|
||||
(e) => {
|
||||
@ -168,55 +143,66 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
||||
},
|
||||
[onMouseEnter],
|
||||
);
|
||||
const renderButton = () => {
|
||||
if (!designable && (field?.data?.hidden || !aclCtx)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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={(props as any).type === 'danger' ? undefined : props.type}
|
||||
>
|
||||
{actionTitle}
|
||||
<Designer {...designerProps} />
|
||||
</SortableItem>
|
||||
);
|
||||
const buttonProps = {
|
||||
designable,
|
||||
field,
|
||||
aclCtx,
|
||||
actionTitle,
|
||||
icon,
|
||||
loading,
|
||||
disabled,
|
||||
buttonStyle,
|
||||
handleMouseEnter,
|
||||
tarComponent,
|
||||
designerProps,
|
||||
componentCls,
|
||||
hashId,
|
||||
className,
|
||||
others,
|
||||
getAriaLabel,
|
||||
type: props.type,
|
||||
Designer,
|
||||
openMode,
|
||||
onClick,
|
||||
refreshDataBlockRequest,
|
||||
service,
|
||||
fieldSchema,
|
||||
setVisible,
|
||||
run,
|
||||
confirm,
|
||||
modal,
|
||||
};
|
||||
|
||||
const buttonElement = renderButton();
|
||||
const buttonElement = RenderButton(buttonProps);
|
||||
|
||||
// if (!btnHover) {
|
||||
// return buttonElement;
|
||||
// }
|
||||
|
||||
const result = (
|
||||
<PopupVisibleProvider visible={false}>
|
||||
<ActionContextProvider
|
||||
button={buttonElement}
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
visible={visible || visibleWithURL}
|
||||
setVisible={(value) => {
|
||||
setVisible?.(value);
|
||||
setVisibleWithURL?.(value);
|
||||
}}
|
||||
formValueChanged={formValueChanged}
|
||||
setFormValueChanged={setFormValueChanged}
|
||||
openMode={openMode}
|
||||
openSize={openSize}
|
||||
containerRefKey={containerRefKey}
|
||||
fieldSchema={fieldSchema}
|
||||
setSubmitted={setParentSubmitted}
|
||||
>
|
||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||
{!popover && renderButton()}
|
||||
{!popover && <RenderButton {...buttonProps} />}
|
||||
<VariablePopupRecordProvider>{!popover && props.children}</VariablePopupRecordProvider>
|
||||
{element}
|
||||
</ActionContextProvider>
|
||||
</PopupVisibleProvider>
|
||||
);
|
||||
|
||||
// fix https://nocobase.height.app/T-3235/description
|
||||
@ -284,3 +270,129 @@ Action.Container = ActionContainer;
|
||||
Action.Page = ActionPage;
|
||||
|
||||
export default Action;
|
||||
|
||||
// TODO: Plugin-related code should not exist in the core. It would be better to implement it by modifying the schema, but it would cause incompatibility.
|
||||
function isBulkEditAction(fieldSchema) {
|
||||
return fieldSchema['x-action'] === 'customize:bulkEdit';
|
||||
}
|
||||
|
||||
function RenderButton({
|
||||
designable,
|
||||
field,
|
||||
aclCtx,
|
||||
actionTitle,
|
||||
icon,
|
||||
loading,
|
||||
disabled,
|
||||
buttonStyle,
|
||||
handleMouseEnter,
|
||||
tarComponent,
|
||||
designerProps,
|
||||
componentCls,
|
||||
hashId,
|
||||
className,
|
||||
others,
|
||||
getAriaLabel,
|
||||
type,
|
||||
Designer,
|
||||
openMode,
|
||||
onClick,
|
||||
refreshDataBlockRequest,
|
||||
service,
|
||||
fieldSchema,
|
||||
setVisible,
|
||||
run,
|
||||
confirm,
|
||||
modal,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { navigateToSubPage } = useNavigateTOSubPage();
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
const { openPopup } = usePagePopup();
|
||||
|
||||
const handleButtonClick = useCallback(
|
||||
(e: React.MouseEvent, checkPortal = true) => {
|
||||
if (checkPortal && isPortalInBody(e.target as Element)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!disabled && aclCtx) {
|
||||
const onOk = () => {
|
||||
if (openMode === 'page') {
|
||||
return navigateToSubPage();
|
||||
}
|
||||
if (onClick) {
|
||||
onClick(e, () => {
|
||||
if (refreshDataBlockRequest !== false) {
|
||||
service?.refresh?.();
|
||||
}
|
||||
});
|
||||
} else if (isBulkEditAction(fieldSchema) || !isPopupVisibleControlledByURL) {
|
||||
setVisible(true);
|
||||
run?.();
|
||||
} else {
|
||||
if (
|
||||
['view', 'update', 'create', 'customize:popup'].includes(fieldSchema['x-action']) &&
|
||||
fieldSchema['x-uid']
|
||||
) {
|
||||
openPopup();
|
||||
} else {
|
||||
setVisible(true);
|
||||
run?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (confirm?.content) {
|
||||
modal.confirm({
|
||||
title: t(confirm.title, { title: actionTitle }),
|
||||
content: t(confirm.content, { title: actionTitle }),
|
||||
onOk,
|
||||
});
|
||||
} else {
|
||||
onOk();
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
aclCtx,
|
||||
actionTitle,
|
||||
confirm?.content,
|
||||
confirm?.title,
|
||||
disabled,
|
||||
modal,
|
||||
onClick,
|
||||
openPopup,
|
||||
refreshDataBlockRequest,
|
||||
run,
|
||||
service,
|
||||
setVisible,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
if (!designable && (field?.data?.hidden || !aclCtx)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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();
|
||||
});
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(getByText('Close'));
|
||||
// page
|
||||
await userEvent.click(getByText('Page'));
|
||||
await userEvent.click(getByText('Open'));
|
||||
|
@ -8,7 +8,6 @@
|
||||
*/
|
||||
|
||||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||
import { useActionContext } from './hooks';
|
||||
import { useDataBlockRequest } from '../../../data-source';
|
||||
import { ActionContextProps } from './types';
|
||||
|
||||
@ -17,11 +16,10 @@ ActionContext.displayName = 'ActionContext';
|
||||
|
||||
export const ActionContextProvider: React.FC<ActionContextProps & { value?: ActionContextProps }> = (props) => {
|
||||
const [submitted, setSubmitted] = useState(false); //是否有提交记录
|
||||
const contextProps = useActionContext();
|
||||
const { visible } = { ...props, ...props.value } || {};
|
||||
const isFirstRender = useRef(true); // 使用ref跟踪是否为首次渲染
|
||||
const service = useDataBlockRequest();
|
||||
const { setSubmitted: setParentSubmitted } = { ...props, ...props.value, ...contextProps };
|
||||
const { setSubmitted: setParentSubmitted } = { ...props, ...props.value };
|
||||
useEffect(() => {
|
||||
if (visible !== undefined) {
|
||||
if (isFirstRender.current) {
|
||||
@ -39,7 +37,7 @@ export const ActionContextProvider: React.FC<ActionContextProps & { value?: Acti
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<ActionContext.Provider value={{ ...contextProps, ...props, ...props?.value, submitted, setSubmitted }}>
|
||||
<ActionContext.Provider value={{ ...props, ...props?.value, submitted, setSubmitted }}>
|
||||
{props.children}
|
||||
</ActionContext.Provider>
|
||||
);
|
||||
|
@ -14,12 +14,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useIsDetailBlock } from '../../../block-provider/FormBlockProvider';
|
||||
import { ActionContext } from './context';
|
||||
|
||||
export const useA = () => {
|
||||
return {
|
||||
async run() {},
|
||||
};
|
||||
};
|
||||
|
||||
export const useActionContext = () => {
|
||||
const ctx = useContext(ActionContext);
|
||||
const { t } = useTranslation();
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { Field } from '@formily/core';
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useCollection, useCollectionManager } from '../../../data-source/collection';
|
||||
import { useCollectionManager } from '../../../data-source/collection';
|
||||
import { markRecordAsNew } from '../../../data-source/collection-record/isNewRecord';
|
||||
import { useSchemaComponentContext } from '../../hooks';
|
||||
import { AssociationFieldContext } from './context';
|
||||
@ -18,8 +18,7 @@ import { AssociationFieldContext } from './context';
|
||||
export const AssociationFieldProvider = observer(
|
||||
(props) => {
|
||||
const field = useField<Field>();
|
||||
const collection = useCollection();
|
||||
const dm = useCollectionManager();
|
||||
const cm = useCollectionManager();
|
||||
const fieldSchema = useFieldSchema();
|
||||
|
||||
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
|
||||
@ -29,12 +28,12 @@ export const AssociationFieldProvider = observer(
|
||||
const allowDissociate = fieldSchema['x-component-props']?.allowDissociate !== false;
|
||||
|
||||
const collectionField = useMemo(
|
||||
() => collection.getField(fieldSchema['x-collection-field']),
|
||||
() => cm.getCollectionField(fieldSchema['x-collection-field']),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fieldSchema['x-collection-field'], fieldSchema.name],
|
||||
);
|
||||
const isFileCollection = useMemo(
|
||||
() => dm.getCollection(collectionField?.target)?.template === 'file',
|
||||
() => cm.getCollection(collectionField?.target)?.template === 'file',
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fieldSchema['x-collection-field']],
|
||||
);
|
||||
|
@ -7,22 +7,20 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { observer, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import React, { Fragment, useRef } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||
import { FormProvider } from '../../core';
|
||||
import { useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
import { useActionContext } from '../action';
|
||||
import { usePagePopup } from '../page/pagePopupUtils';
|
||||
import { transformNestedData } from './InternalCascadeSelect';
|
||||
import { ButtonListProps, ReadPrettyInternalViewer, isObject } from './InternalViewer';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
import schema from './schema';
|
||||
import { getTabFormatValue, useLabelUiSchema } from './util';
|
||||
import { transformNestedData } from './InternalCascadeSelect';
|
||||
import { isObject } from './InternalViewer';
|
||||
|
||||
interface IEllipsisWithTooltipRef {
|
||||
setPopoverVisible: (boolean) => void;
|
||||
@ -34,18 +32,13 @@ const toValue = (value, placeholder) => {
|
||||
}
|
||||
return value;
|
||||
};
|
||||
export const ReadPrettyInternalTag: React.FC = observer(
|
||||
(props: any) => {
|
||||
|
||||
const ButtonTabList: React.FC<ButtonListProps> = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const recordCtx = useRecord();
|
||||
const { enableLink, tagColorField } = fieldSchema['x-component-props'];
|
||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||
const field = useField();
|
||||
const fieldNames = useFieldNames(props);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const fieldNames = useFieldNames({ fieldNames: props.fieldNames });
|
||||
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');
|
||||
@ -54,7 +47,8 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const isTreeCollection = targetCollection?.template === 'tree';
|
||||
const [btnHover, setBtnHover] = useState(false);
|
||||
const { openPopup } = usePagePopup();
|
||||
const recordData = useCollectionRecordData();
|
||||
|
||||
const renderRecords = () =>
|
||||
toArr(props.value).map((record, index, arr) => {
|
||||
@ -76,17 +70,19 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onMouseEnter={() => {
|
||||
setBtnHover(true);
|
||||
props.setBtnHover(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setBtnHover(true);
|
||||
props.setBtnHover(true);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (designable) {
|
||||
insertViewer(schema.Viewer);
|
||||
}
|
||||
setVisible(true);
|
||||
setRecord(record);
|
||||
openPopup({
|
||||
recordData: record,
|
||||
parentRecordData: recordData,
|
||||
});
|
||||
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
|
||||
}}
|
||||
>
|
||||
@ -101,59 +97,12 @@ export const ReadPrettyInternalTag: React.FC = observer(
|
||||
);
|
||||
});
|
||||
|
||||
const btnElement = (
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
</EllipsisWithTooltip>
|
||||
);
|
||||
return <>{renderRecords()}</>;
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
export const ReadPrettyInternalTag: React.FC = observer(
|
||||
(props: any) => {
|
||||
return <ReadPrettyInternalViewer {...props} ButtonList={ButtonTabList} />;
|
||||
},
|
||||
{ displayName: 'ReadPrettyInternalTag' },
|
||||
);
|
||||
|
@ -9,18 +9,16 @@
|
||||
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { toArr } from '@formily/shared';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import React, { FC, Fragment, useRef, useState } from 'react';
|
||||
import { useDesignable } from '../../';
|
||||
import { BlockAssociationContext, WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { CollectionProvider_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||
import { Collection } from '../../../data-source';
|
||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||
import { useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||
import { RecordProvider, useRecord } from '../../../record-provider';
|
||||
import { FormProvider } from '../../core';
|
||||
import { useCompile } from '../../hooks';
|
||||
import { ActionContextProvider, useActionContext } from '../action';
|
||||
import { EllipsisWithTooltip } from '../input/EllipsisWithTooltip';
|
||||
import { PopupVisibleProvider } from '../page/PagePopups';
|
||||
import { usePagePopup } from '../page/pagePopupUtils';
|
||||
import { useAssociationFieldContext, useFieldNames, useInsertSchema } from './hooks';
|
||||
import { transformNestedData } from './InternalCascadeSelect';
|
||||
import schema from './schema';
|
||||
@ -39,28 +37,32 @@ const toValue = (value, placeholder) => {
|
||||
export function isObject(value) {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
(props: any) => {
|
||||
|
||||
export interface ButtonListProps {
|
||||
value: any;
|
||||
setBtnHover: any;
|
||||
fieldNames?: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
const ButtonLinkList: FC<ButtonListProps> = (props) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const recordCtx = useRecord();
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const cm = useCollectionManager();
|
||||
const { enableLink } = fieldSchema['x-component-props'] || {};
|
||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||
const field = useField();
|
||||
const fieldNames = useFieldNames(props);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const fieldNames = useFieldNames({ fieldNames: props.fieldNames });
|
||||
const insertViewer = useInsertSchema('Viewer');
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const [record, setRecord] = useState({});
|
||||
const compile = useCompile();
|
||||
const { designable } = useDesignable();
|
||||
const { snapshot } = useActionContext();
|
||||
const targetCollection = getCollection(collectionField?.target);
|
||||
const targetCollection = cm.getCollection(collectionField?.target);
|
||||
const isTreeCollection = targetCollection?.template === 'tree';
|
||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||
const getLabelUiSchema = useLabelUiSchemaV2();
|
||||
const [btnHover, setBtnHover] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { openPopup } = usePagePopup();
|
||||
const recordData = useCollectionRecordData();
|
||||
|
||||
const renderRecords = () =>
|
||||
toArr(props.value).map((record, index, arr) => {
|
||||
@ -86,17 +88,19 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
) : enableLink !== false ? (
|
||||
<a
|
||||
onMouseEnter={() => {
|
||||
setBtnHover(true);
|
||||
props.setBtnHover(true);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setBtnHover(true);
|
||||
props.setBtnHover(true);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (designable) {
|
||||
insertViewer(schema.Viewer);
|
||||
}
|
||||
setVisible(true);
|
||||
setRecord(record);
|
||||
openPopup({
|
||||
recordData: record,
|
||||
parentRecordData: recordData,
|
||||
});
|
||||
ellipsisWithTooltipRef?.current?.setPopoverVisible(false);
|
||||
}}
|
||||
>
|
||||
@ -111,19 +115,45 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
);
|
||||
});
|
||||
|
||||
return <>{renderRecords()}</>;
|
||||
};
|
||||
|
||||
interface ReadPrettyInternalViewerProps {
|
||||
ButtonList: FC<ButtonListProps>;
|
||||
value: any;
|
||||
fieldNames?: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
(props: ReadPrettyInternalViewerProps) => {
|
||||
const { value, ButtonList = ButtonLinkList } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { enableLink } = fieldSchema['x-component-props'] || {};
|
||||
// value 做了转换,但 props.value 和原来 useField().value 的值不一致
|
||||
const field = useField();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { options: collectionField } = useAssociationFieldContext();
|
||||
const ellipsisWithTooltipRef = useRef<IEllipsisWithTooltipRef>();
|
||||
const { visibleWithURL, setVisibleWithURL } = usePagePopup();
|
||||
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
||||
|
||||
const btnElement = (
|
||||
<EllipsisWithTooltip ellipsis={true} ref={ellipsisWithTooltipRef}>
|
||||
{renderRecords()}
|
||||
<ButtonList setBtnHover={setBtnHover} value={value} fieldNames={props.fieldNames} />
|
||||
</EllipsisWithTooltip>
|
||||
);
|
||||
|
||||
if (enableLink === false || !btnHover) {
|
||||
return btnElement;
|
||||
}
|
||||
|
||||
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}>
|
||||
<FormProvider>
|
||||
<RecursionField
|
||||
schema={fieldSchema}
|
||||
onlyRenderProperties
|
||||
@ -132,44 +162,28 @@ export const ReadPrettyInternalViewer: React.FC = observer(
|
||||
return s['x-component'] === 'AssociationField.Viewer';
|
||||
}}
|
||||
/>
|
||||
</FormProvider>
|
||||
</WithoutTableFieldResource.Provider>
|
||||
</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 (
|
||||
<div>
|
||||
<BlockAssociationContext.Provider value={`${collectionField?.collectionName}.${collectionField?.name}`}>
|
||||
<CollectionProvider_deprecated name={collectionField?.target ?? collectionField?.targetCollection}>
|
||||
{btnElement}
|
||||
<PopupVisibleProvider visible={false}>
|
||||
<ActionContextProvider
|
||||
value={{
|
||||
visible,
|
||||
setVisible,
|
||||
visible: visible || visibleWithURL,
|
||||
setVisible: (value) => {
|
||||
setVisible?.(value);
|
||||
setVisibleWithURL?.(value);
|
||||
},
|
||||
openMode: 'drawer',
|
||||
snapshot: collectionField?.interface === 'snapshot',
|
||||
fieldSchema: fieldSchema,
|
||||
}}
|
||||
>
|
||||
{renderRecordProvider()}
|
||||
{btnElement}
|
||||
{renderWithoutTableFieldResourceProvider()}
|
||||
</ActionContextProvider>
|
||||
</CollectionProvider_deprecated>
|
||||
</BlockAssociationContext.Provider>
|
||||
</div>
|
||||
</PopupVisibleProvider>
|
||||
);
|
||||
},
|
||||
{ displayName: 'ReadPrettyInternalViewer' },
|
||||
|
@ -185,7 +185,14 @@ export default function useServiceOptions(props) {
|
||||
}, [collectionField?.target, action, filter, service]);
|
||||
}
|
||||
|
||||
export const useFieldNames = (props) => {
|
||||
export const useFieldNames = (
|
||||
props: {
|
||||
fieldNames?: {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
} = {},
|
||||
) => {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const fieldNames =
|
||||
fieldSchema['x-component-props']?.['field']?.['uiSchema']?.['x-component-props']?.['fieldNames'] ||
|
||||
|
@ -7,19 +7,19 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { Button, Result, Typography } from 'antd';
|
||||
import React, { FC } from 'react';
|
||||
import { FallbackProps } from 'react-error-boundary';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { ErrorFallbackModal } from './ErrorFallbackModal';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocationNoUpdate } from '../../../application';
|
||||
import { ErrorFallbackModal } from './ErrorFallbackModal';
|
||||
|
||||
const { Paragraph, Text, Link } = Typography;
|
||||
|
||||
export const useDownloadLogs = (error: any, data: Record<string, any> = {}) => {
|
||||
const location = useLocation();
|
||||
const location = useLocationNoUpdate();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const api = useAPIClient();
|
||||
return {
|
||||
|
@ -32,7 +32,8 @@ const InternalGridCardBlockProvider = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!service?.loading) {
|
||||
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
|
||||
// @ts-ignore
|
||||
form.fields[field.address.concat('list').toString()]?.setValue(service?.data?.data);
|
||||
}
|
||||
}, [field.address, form, service?.data?.data, service?.loading]);
|
||||
|
||||
|
@ -32,7 +32,10 @@ const InternalListBlockProvider = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!service?.loading) {
|
||||
form.setValuesIn(field.address.concat('list').toString(), service?.data?.data);
|
||||
form.query(/\.list$/).forEach((field) => {
|
||||
// @ts-ignore
|
||||
field.setValue?.(service?.data?.data);
|
||||
});
|
||||
}
|
||||
}, [field.address, form, service?.data?.data, service?.loading]);
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { ArrayField } from '@formily/core';
|
||||
import { RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import { List as AntdList, PaginationProps, theme } from 'antd';
|
||||
|
@ -10,7 +10,7 @@
|
||||
import { TreeSelect } from '@formily/antd-v5';
|
||||
import { Field, onFieldChange } from '@formily/core';
|
||||
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { findByUid } from '.';
|
||||
import { createDesignable, useCompile } from '../..';
|
||||
@ -210,6 +210,8 @@ const InsertMenuItems = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const components = { TreeSelect };
|
||||
|
||||
export const MenuDesigner = () => {
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -218,9 +220,13 @@ export const MenuDesigner = () => {
|
||||
const { t } = useTranslation();
|
||||
const menuSchema = findMenuSchema(fieldSchema);
|
||||
const compile = useCompile();
|
||||
const onSelect = compile(menuSchema?.['x-component-props']?.['onSelect']);
|
||||
const items = toItems(menuSchema?.properties);
|
||||
const effects = (form) => {
|
||||
const onSelect = useMemo(
|
||||
() => compile(menuSchema?.['x-component-props']?.['onSelect']),
|
||||
[menuSchema?.['x-component-props']?.['onSelect']],
|
||||
);
|
||||
const items = useMemo(() => toItems(menuSchema?.properties), [menuSchema?.properties]);
|
||||
const effects = useCallback(
|
||||
(form) => {
|
||||
onFieldChange('target', (field: Field) => {
|
||||
const [, component] = field?.value?.split?.('||') || [];
|
||||
field.query('position').take((f: Field) => {
|
||||
@ -237,8 +243,11 @@ export const MenuDesigner = () => {
|
||||
];
|
||||
});
|
||||
});
|
||||
};
|
||||
const schema = {
|
||||
},
|
||||
[t],
|
||||
);
|
||||
const schema = useMemo(() => {
|
||||
return {
|
||||
type: 'object',
|
||||
title: t('Edit menu item'),
|
||||
properties: {
|
||||
@ -256,10 +265,13 @@ export const MenuDesigner = () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
const initialValues = {
|
||||
}, [t]);
|
||||
const initialValues = useMemo(() => {
|
||||
return {
|
||||
title: field.title,
|
||||
icon: field.componentProps.icon,
|
||||
};
|
||||
}, [field]);
|
||||
if (fieldSchema['x-component'] === 'Menu.URL') {
|
||||
schema.properties['href'] = {
|
||||
title: t('Link'),
|
||||
@ -268,14 +280,8 @@ export const MenuDesigner = () => {
|
||||
};
|
||||
initialValues['href'] = field.componentProps.href;
|
||||
}
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Edit')}
|
||||
eventKey="edit"
|
||||
schema={schema as ISchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={({ title, icon, href }) => {
|
||||
const onEditSubmit: (values: any) => void = useCallback(
|
||||
({ title, icon, href }) => {
|
||||
const schema = {
|
||||
['x-uid']: fieldSchema['x-uid'],
|
||||
'x-server-hooks': [
|
||||
@ -301,15 +307,12 @@ export const MenuDesigner = () => {
|
||||
dn.emit('patch', {
|
||||
schema,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Move to')}
|
||||
eventKey="move-to"
|
||||
components={{ TreeSelect }}
|
||||
effects={effects}
|
||||
schema={
|
||||
{
|
||||
},
|
||||
[fieldSchema, field, dn, refresh, onSelect],
|
||||
);
|
||||
|
||||
const modalSchema = useMemo(() => {
|
||||
return {
|
||||
type: 'object',
|
||||
title: t('Move to'),
|
||||
properties: {
|
||||
@ -333,9 +336,11 @@ export const MenuDesigner = () => {
|
||||
'x-decorator': 'FormItem',
|
||||
},
|
||||
},
|
||||
} as ISchema
|
||||
}
|
||||
onSubmit={({ target, position }) => {
|
||||
} as ISchema;
|
||||
}, [items, t]);
|
||||
|
||||
const onMoveToSubmit: (values: any) => void = useCallback(
|
||||
({ target, position }) => {
|
||||
const [uid] = target?.split?.('||') || [];
|
||||
if (!uid) {
|
||||
return;
|
||||
@ -349,18 +354,39 @@ export const MenuDesigner = () => {
|
||||
});
|
||||
dn.loadAPIClientEvents();
|
||||
dn.insertAdjacent(position, fieldSchema);
|
||||
}}
|
||||
},
|
||||
[fieldSchema, menuSchema, t, api, refresh],
|
||||
);
|
||||
|
||||
const removeConfirmTitle = useMemo(() => {
|
||||
return {
|
||||
title: t('Delete menu item'),
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Edit')}
|
||||
eventKey="edit"
|
||||
schema={schema as ISchema}
|
||||
initialValues={initialValues}
|
||||
onSubmit={onEditSubmit}
|
||||
/>
|
||||
<SchemaSettingsModalItem
|
||||
title={t('Move to')}
|
||||
eventKey="move-to"
|
||||
components={components}
|
||||
effects={effects}
|
||||
schema={modalSchema}
|
||||
onSubmit={onMoveToSubmit}
|
||||
/>
|
||||
<SchemaSettingsDivider />
|
||||
<InsertMenuItems eventKey={'insertbeforeBegin'} title={t('Insert before')} insertPosition={'beforeBegin'} />
|
||||
<InsertMenuItems eventKey={'insertafterEnd'} title={t('Insert after')} insertPosition={'afterEnd'} />
|
||||
<InsertMenuItems eventKey={'insertbeforeEnd'} title={t('Insert inner')} insertPosition={'beforeEnd'} />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
confirm={{
|
||||
title: t('Delete menu item'),
|
||||
}}
|
||||
/>
|
||||
<SchemaSettingsRemove confirm={removeConfirmTitle} />
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
import { uid } from '@formily/shared';
|
||||
import { error } from '@nocobase/utils/client';
|
||||
import { Menu as AntdMenu, MenuProps } from 'antd';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createDesignable, DndContext, SortableItem, useDesignable, useDesigner } from '../..';
|
||||
@ -229,13 +229,8 @@ const HeaderMenu = ({
|
||||
return result;
|
||||
}, [children, designable]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component />
|
||||
<AntdMenu
|
||||
{...others}
|
||||
className={headerMenuClass}
|
||||
onSelect={(info: any) => {
|
||||
const handleSelect = useCallback(
|
||||
(info: any) => {
|
||||
const s = schema.properties?.[info.key];
|
||||
|
||||
if (!s) {
|
||||
@ -269,7 +264,17 @@ const HeaderMenu = ({
|
||||
} else {
|
||||
onSelect?.(info);
|
||||
}
|
||||
}}
|
||||
},
|
||||
[schema, mode, onSelect, setLoading, setDefaultSelectedKeys],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Component />
|
||||
<AntdMenu
|
||||
{...others}
|
||||
className={headerMenuClass}
|
||||
onSelect={handleSelect}
|
||||
mode={mode === 'mix' ? 'horizontal' : mode}
|
||||
defaultOpenKeys={defaultOpenKeys}
|
||||
defaultSelectedKeys={defaultSelectedKeys}
|
||||
@ -347,12 +352,8 @@ const SideMenu = ({
|
||||
mode={'inline'}
|
||||
openKeys={openKeys}
|
||||
selectedKeys={selectedKeys}
|
||||
onSelect={(info) => {
|
||||
onSelect?.(info);
|
||||
}}
|
||||
onOpenChange={(openKeys) => {
|
||||
setOpenKeys(openKeys);
|
||||
}}
|
||||
onSelect={onSelect}
|
||||
onOpenChange={setOpenKeys}
|
||||
className={sideMenuClass}
|
||||
items={items as MenuProps['items']}
|
||||
/>
|
||||
@ -496,6 +497,14 @@ export const Menu: ComposedMenu = observer(
|
||||
{ displayName: 'Menu' },
|
||||
);
|
||||
|
||||
const menuItemTitleStyle = {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
verticalAlign: 'middle',
|
||||
};
|
||||
|
||||
Menu.Item = observer(
|
||||
(props) => {
|
||||
const { t } = useMenuTranslation();
|
||||
@ -521,17 +530,7 @@ Menu.Item = observer(
|
||||
removeParentsIfNoChildren={false}
|
||||
>
|
||||
<Icon type={icon} />
|
||||
<span
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
verticalAlign: 'middle',
|
||||
}}
|
||||
>
|
||||
{t(field.title)}
|
||||
</span>
|
||||
<span style={menuItemTitleStyle}>{t(field.title)}</span>
|
||||
<Designer />
|
||||
</SortableItem>
|
||||
</FieldContext.Provider>
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { genStyleHook } from './../__builtins__';
|
||||
import { genStyleHook } from '../__builtins__';
|
||||
|
||||
export const useStyles = genStyleHook('nb-page', (token) => {
|
||||
const { componentCls } = token;
|
@ -13,13 +13,14 @@ import { FormLayout } from '@formily/antd-v5';
|
||||
import { Schema, SchemaOptionsContext, useFieldSchema } from '@formily/react';
|
||||
import { Button, Tabs } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Outlet, useOutletContext, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { FormDialog } from '..';
|
||||
import { useStyles as useAClStyles } from '../../../acl/style';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useNavigateNoUpdate } from '../../../application/CustomRouterContextProvider';
|
||||
import { useAppSpin } from '../../../application/hooks/useAppSpin';
|
||||
import { useDocumentTitle } from '../../../document-title';
|
||||
import { useGlobalTheme } from '../../../global-theme';
|
||||
@ -32,8 +33,8 @@ import { useCompile, useDesignable } from '../../hooks';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { ErrorFallback } from '../error-fallback';
|
||||
import FixedBlock from './FixedBlock';
|
||||
import { useStyles } from './Page.style';
|
||||
import { PageDesigner, PageTabDesigner } from './PageTabDesigner';
|
||||
import { useStyles } from './style';
|
||||
|
||||
export const Page = (props) => {
|
||||
const { children, ...others } = props;
|
||||
@ -44,6 +45,7 @@ export const Page = (props) => {
|
||||
const dn = useDesignable();
|
||||
const { theme } = useGlobalTheme();
|
||||
const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer();
|
||||
const { tabUid, name: pageUid } = useParams();
|
||||
|
||||
// react18 tab 动画会卡顿,所以第一个 tab 时,动画禁用,后面的 tab 才启用
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
@ -62,11 +64,13 @@ export const Page = (props) => {
|
||||
const enablePageTabs = fieldSchema['x-component-props']?.enablePageTabs;
|
||||
const hidePageTitle = fieldSchema['x-component-props']?.hidePageTitle;
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const activeKey = useMemo(
|
||||
() => searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(),
|
||||
[fieldSchema.properties, searchParams],
|
||||
// 处理 searchParams 是为了兼容旧版的 tab 参数
|
||||
() => tabUid || searchParams.get('tab') || Object.keys(fieldSchema.properties || {}).shift(),
|
||||
[fieldSchema.properties, searchParams, tabUid],
|
||||
);
|
||||
const [height, setHeight] = useState(0);
|
||||
const { wrapSSR, hashId, componentCls } = useStyles();
|
||||
@ -87,27 +91,12 @@ export const Page = (props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const handleErrors = (error) => {
|
||||
const handleErrors = useCallback((error) => {
|
||||
console.error(error);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return wrapSSR(
|
||||
<div className={`${componentCls} ${hashId} ${aclStyles.styles}`}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
|
||||
}}
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={classNames('pageHeaderCss', pageHeaderTitle || enablePageTabs ? '' : 'height0')}
|
||||
ghost={false}
|
||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||
title={pageHeaderTitle || ' '}
|
||||
{...others}
|
||||
footer={
|
||||
enablePageTabs && (
|
||||
const footer = useMemo(() => {
|
||||
return enablePageTabs ? (
|
||||
<DndContext>
|
||||
<Tabs
|
||||
size={'small'}
|
||||
@ -122,7 +111,7 @@ export const Page = (props) => {
|
||||
}}
|
||||
onTabClick={(activeKey) => {
|
||||
setLoading(true);
|
||||
setSearchParams([['tab', activeKey]]);
|
||||
navigate(`/admin/${pageUid}/tabs/${activeKey}`);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 50);
|
||||
@ -199,31 +188,85 @@ export const Page = (props) => {
|
||||
})}
|
||||
/>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
) : null;
|
||||
}, [
|
||||
hasMounted,
|
||||
activeKey,
|
||||
fieldSchema,
|
||||
dn.designable,
|
||||
options.scope,
|
||||
options.components,
|
||||
pageUid,
|
||||
fieldSchema.mapProperties((schema) => schema.title || t('Unnamed')).join(),
|
||||
enablePageTabs,
|
||||
]);
|
||||
|
||||
return wrapSSR(
|
||||
<div className={`${componentCls} ${hashId} ${aclStyles.styles}`}>
|
||||
<PageDesigner title={fieldSchema.title || title} />
|
||||
<div
|
||||
ref={(ref) => {
|
||||
setHeight(Math.floor(ref?.getBoundingClientRect().height || 0) + 1);
|
||||
}}
|
||||
>
|
||||
{!disablePageHeader && (
|
||||
<AntdPageHeader
|
||||
className={classNames('pageHeaderCss', pageHeaderTitle || enablePageTabs ? '' : 'height0')}
|
||||
ghost={false}
|
||||
// 如果标题为空的时候会导致 PageHeader 不渲染,所以这里设置一个空白字符,然后再设置高度为 0
|
||||
title={pageHeaderTitle || ' '}
|
||||
{...others}
|
||||
footer={footer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="nb-page-wrapper">
|
||||
<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>
|
||||
</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';
|
||||
|
||||
function PageContent(
|
||||
loading: boolean,
|
||||
disablePageHeader: any,
|
||||
enablePageTabs: any,
|
||||
fieldSchema: Schema<any, any, any, any, any, any, any, any, any>,
|
||||
activeKey: string,
|
||||
height: number,
|
||||
props: any,
|
||||
): React.ReactNode {
|
||||
const PageContent = memo(
|
||||
({
|
||||
loading,
|
||||
disablePageHeader,
|
||||
enablePageTabs,
|
||||
fieldSchema,
|
||||
activeKey,
|
||||
height,
|
||||
}: {
|
||||
loading: boolean;
|
||||
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();
|
||||
|
||||
@ -231,7 +274,9 @@ function PageContent(
|
||||
return render();
|
||||
}
|
||||
|
||||
return !disablePageHeader && enablePageTabs ? (
|
||||
return (
|
||||
<>
|
||||
{!disablePageHeader && enablePageTabs ? (
|
||||
fieldSchema.mapProperties((schema) => {
|
||||
if (schema.name !== activeKey) return null;
|
||||
|
||||
@ -256,5 +301,9 @@ function PageContent(
|
||||
<SchemaComponent schema={fieldSchema} distributed />
|
||||
</div>
|
||||
</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.
|
||||
*/
|
||||
|
||||
export * from './Page';
|
||||
export * from './FixedBlock';
|
||||
export * from './PageTab.Settings';
|
||||
export * from './Page.Settings';
|
||||
export * from './FixedBlockDesignerItem';
|
||||
export * from './Page';
|
||||
export * from './Page.Settings';
|
||||
export * from './PageTab.Settings';
|
||||
export { PopupSettingsProvider } from './PopupSettingsProvider';
|
||||
|
@ -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 { Schema } from '@formily/react';
|
||||
import { Spin } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useRequest } from '../../api-client';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useSchemaComponentContext } from '../hooks';
|
||||
import { FormProvider } from './FormProvider';
|
||||
import { SchemaComponent } from './SchemaComponent';
|
||||
import { useRequestSchema } from './useRequestSchema';
|
||||
|
||||
export interface RemoteSchemaComponentProps {
|
||||
scope?: any;
|
||||
@ -42,15 +42,15 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
schemaTransform = defaultTransform,
|
||||
} = props;
|
||||
const { reset } = useSchemaComponentContext();
|
||||
const type = onlyRenderProperties ? 'getProperties' : 'getJsonSchema';
|
||||
const conf = {
|
||||
url: `/uiSchemas:${onlyRenderProperties ? 'getProperties' : 'getJsonSchema'}/${uid}`,
|
||||
url: `/uiSchemas:${type}/${uid}`,
|
||||
};
|
||||
const form = useMemo(() => createForm(), [uid]);
|
||||
const { data, loading } = useRequest<{
|
||||
data: any;
|
||||
}>(conf, {
|
||||
refreshDeps: [uid],
|
||||
onSuccess(data) {
|
||||
const { schema, loading } = useRequestSchema({
|
||||
uid,
|
||||
type,
|
||||
onSuccess: (data) => {
|
||||
onSuccess && onSuccess(data);
|
||||
reset && reset();
|
||||
},
|
||||
@ -62,14 +62,15 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
|
||||
return <Spin />;
|
||||
}
|
||||
return noForm ? (
|
||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(data?.data || {})} />
|
||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||
) : (
|
||||
<FormProvider form={form}>
|
||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(data?.data || {})} />
|
||||
<SchemaComponent memoized components={components} scope={scope} schema={schemaTransform(schema || {})} />
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) => {
|
||||
export const RemoteSchemaComponent: React.FC<RemoteSchemaComponentProps> = memo((props) => {
|
||||
return props.uid ? <RequestSchemaComponent {...props} /> : null;
|
||||
};
|
||||
});
|
||||
RemoteSchemaComponent.displayName = 'RemoteSchemaComponent';
|
||||
|
@ -8,10 +8,10 @@
|
||||
*/
|
||||
|
||||
import { IRecursionFieldProps, ISchemaFieldProps, RecursionField, Schema } from '@formily/react';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { useUpdate } from 'ahooks';
|
||||
import React, { memo, useContext, useMemo } from 'react';
|
||||
import { SchemaComponentContext } from '../context';
|
||||
import { SchemaComponentOptions } from './SchemaComponentOptions';
|
||||
import { useUpdate } from 'ahooks';
|
||||
|
||||
type SchemaComponentOnChange = {
|
||||
onChange?: (s: Schema) => void;
|
||||
@ -44,10 +44,10 @@ interface DistributedProps {
|
||||
distributed?: boolean;
|
||||
}
|
||||
|
||||
const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { components, scope, schema, distributed, ...others } = props;
|
||||
const RecursionSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { components, scope, schema: _schema, distributed, ...others } = props;
|
||||
const ctx = useContext(SchemaComponentContext);
|
||||
const s = useMemo(() => toSchema(schema), [schema]);
|
||||
const schema = useMemo(() => toSchema(_schema), [_schema]);
|
||||
const refresh = useUpdate();
|
||||
|
||||
return (
|
||||
@ -60,30 +60,32 @@ const RecursionSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnCh
|
||||
if (ctx.distributed === false || distributed === false) {
|
||||
ctx.refresh?.();
|
||||
}
|
||||
props.onChange?.(s);
|
||||
props.onChange?.(schema);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions inherit components={components} scope={scope}>
|
||||
<RecursionField {...others} schema={s} />
|
||||
<RecursionField {...others} schema={schema} />
|
||||
</SchemaComponentOptions>
|
||||
</SchemaComponentContext.Provider>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const MemoizedSchemaComponent = (props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const MemoizedSchemaComponent = memo((props: ISchemaFieldProps & SchemaComponentOnChange & DistributedProps) => {
|
||||
const { schema, ...others } = props;
|
||||
const s = useMemoizedSchema(schema);
|
||||
return <RecursionSchemaComponent {...others} schema={s} />;
|
||||
};
|
||||
});
|
||||
|
||||
export const SchemaComponent = (
|
||||
export const SchemaComponent = memo(
|
||||
(
|
||||
props: (ISchemaFieldProps | IRecursionFieldProps) & { memoized?: boolean } & SchemaComponentOnChange &
|
||||
DistributedProps,
|
||||
) => {
|
||||
) => {
|
||||
const { memoized, ...others } = props;
|
||||
if (memoized) {
|
||||
return <MemoizedSchemaComponent {...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 { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { observer, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import React, { useEffect, useMemo, useState, forwardRef, createRef } from 'react';
|
||||
import { composeRef } from 'rc-util/lib/ref';
|
||||
import { useDesignable } from '../../';
|
||||
import { useACLRolesCheck, useRecordPkValue, useACLActionParamsContext } from '../../acl/ACLProvider';
|
||||
import {
|
||||
CollectionProvider_deprecated,
|
||||
useCollection_deprecated,
|
||||
useCollectionManager_deprecated,
|
||||
} from '../../collection-manager';
|
||||
import React, { createRef, forwardRef, useEffect, useMemo } from 'react';
|
||||
import { Collection, useDesignable } from '../../';
|
||||
import { useACLActionParamsContext, useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
|
||||
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../collection-manager';
|
||||
import { useTreeParentRecord } from '../../modules/blocks/data-blocks/table/TreeRecordProvider';
|
||||
import { useRecord } from '../../record-provider';
|
||||
import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component';
|
||||
import { useCompile } from '../../schema-component';
|
||||
import { linkageAction } from '../../schema-component/antd/action/utils';
|
||||
import { useNavigateTOSubPage } from '../../schema-component/antd/page/SubPages';
|
||||
import { usePagePopup } from '../../schema-component/antd/page/pagePopupUtils';
|
||||
import { parseVariables } from '../../schema-component/common/utils/uitls';
|
||||
import { useLocalVariables, useVariables } from '../../variables';
|
||||
|
||||
@ -66,17 +65,17 @@ function useAclCheckFn() {
|
||||
}
|
||||
|
||||
const InternalCreateRecordAction = (props: any, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const collection = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
|
||||
const field: any = useField();
|
||||
const [currentCollection, setCurrentCollection] = useState(collection.name);
|
||||
const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource);
|
||||
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
|
||||
const values = useRecord();
|
||||
const ctx = useActionContext();
|
||||
const variables = useVariables();
|
||||
const localVariables = useLocalVariables({ currentForm: { values } as any });
|
||||
const { openPopup } = usePagePopup();
|
||||
const { navigateToSubPage } = useNavigateTOSubPage();
|
||||
const treeRecordData = useTreeParentRecord();
|
||||
|
||||
useEffect(() => {
|
||||
field.stateOfLinkageRules = {};
|
||||
linkageRules
|
||||
@ -95,26 +94,26 @@ const InternalCreateRecordAction = (props: any, ref) => {
|
||||
}, [field, linkageRules, localVariables, variables]);
|
||||
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||
const buttonRef = composeRef(ref, internalRef);
|
||||
|
||||
return (
|
||||
//@ts-ignore
|
||||
<div ref={buttonRef as React.Ref<HTMLButtonElement>}>
|
||||
<CreateAction
|
||||
{...props}
|
||||
onClick={(collectionData) => {
|
||||
if (collectionData.name === collection.name) {
|
||||
ctx?.setVisible(true);
|
||||
} else {
|
||||
setVisible(true);
|
||||
onClick={(collection: Collection) => {
|
||||
if (openMode === 'page') {
|
||||
return navigateToSubPage();
|
||||
}
|
||||
|
||||
if (treeRecordData) {
|
||||
openPopup({
|
||||
recordData: treeRecordData,
|
||||
});
|
||||
} else {
|
||||
openPopup();
|
||||
}
|
||||
setCurrentCollection(collectionData.name);
|
||||
setCurrentCollectionDataSource(collectionData.dataSource);
|
||||
}}
|
||||
/>
|
||||
<ActionContextProvider value={{ ...ctx, fieldSchema, visible, setVisible }}>
|
||||
<CollectionProvider_deprecated name={currentCollection} dataSource={currentCollectionDataSource}>
|
||||
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
|
||||
</CollectionProvider_deprecated>
|
||||
</ActionContextProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -9,15 +9,17 @@
|
||||
|
||||
import { useField, useFieldSchema } from '@formily/react';
|
||||
import { Select } from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializerItem, SchemaInitializerSelect } from '../application';
|
||||
import { useDesignable } from '../schema-component';
|
||||
import { usePopupSettings } from '../schema-component/antd/page/PopupSettingsProvider';
|
||||
import { SchemaSettingsSelectItem } from '../schema-settings';
|
||||
|
||||
interface Options {
|
||||
openMode?: boolean;
|
||||
openSize?: boolean;
|
||||
modeOptions?: { label: string; value: string }[];
|
||||
}
|
||||
export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options) => {
|
||||
const { openMode = true, openSize = true } = options;
|
||||
@ -25,17 +27,29 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options)
|
||||
const field = useField();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer';
|
||||
const modeOptions = useMemo(() => {
|
||||
if (isPopupVisibleControlledByURL) {
|
||||
return [
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
{ label: t('Page'), value: 'page' },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
];
|
||||
}, [t, isPopupVisibleControlledByURL]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{openMode ? (
|
||||
<SchemaInitializerSelect
|
||||
title={t('Open mode')}
|
||||
options={[
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
]}
|
||||
options={modeOptions}
|
||||
value={openModeValue}
|
||||
onChange={(value) => {
|
||||
field.componentProps.openMode = value;
|
||||
@ -92,23 +106,40 @@ export const SchemaInitializerOpenModeSchemaItems: React.FC<Options> = (options)
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaSettingOpenModeSchemaItems: React.FC<Options> = (options) => {
|
||||
const { openMode = true, openSize = true } = options;
|
||||
export const SchemaSettingOpenModeSchemaItems: React.FC<Options> = (props) => {
|
||||
const { openMode = true, openSize = true, modeOptions } = props;
|
||||
const fieldSchema = useFieldSchema();
|
||||
const field = useField();
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||
const openModeValue = fieldSchema?.['x-component-props']?.['openMode'] || 'drawer';
|
||||
|
||||
const _modeOptions = useMemo(() => {
|
||||
if (modeOptions) {
|
||||
return modeOptions;
|
||||
}
|
||||
|
||||
if (isPopupVisibleControlledByURL) {
|
||||
return [
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
{ label: t('Page'), value: 'page' },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
];
|
||||
}, [modeOptions, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{openMode ? (
|
||||
<SchemaSettingsSelectItem
|
||||
title={t('Open mode')}
|
||||
options={[
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
]}
|
||||
options={_modeOptions}
|
||||
value={openModeValue}
|
||||
onChange={(value) => {
|
||||
field.componentProps.openMode = value;
|
||||
|
@ -43,10 +43,9 @@ import React, {
|
||||
} from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { APIClientProvider } from '../api-client/APIClientProvider';
|
||||
import { useAPIClient } from '../api-client/hooks/useAPIClient';
|
||||
import { ApplicationContext, useApp } from '../application';
|
||||
import { ApplicationContext, LocationSearchContext, useApp, useLocationSearch } from '../application';
|
||||
import {
|
||||
BlockContext,
|
||||
BlockRequestContext_deprecated,
|
||||
@ -746,6 +745,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
||||
const { association } = useDataBlockProps() || {};
|
||||
const formCtx = useFormBlockContext();
|
||||
const blockOptions = useBlockContext();
|
||||
const locationSearch = useLocationSearch();
|
||||
|
||||
// 解决变量`当前对象`值在弹窗中丢失的问题
|
||||
const { formValue: subFormValue, collection: subFormCollection } = useSubFormValue();
|
||||
@ -784,7 +784,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
||||
name="form"
|
||||
getActiveFieldsName={upLevelActiveFields?.getActiveFieldsName}
|
||||
>
|
||||
<Router location={location} navigator={null}>
|
||||
<LocationSearchContext.Provider value={locationSearch}>
|
||||
<BlockRequestContext_deprecated.Provider value={ctx}>
|
||||
<DataSourceApplicationProvider dataSourceManager={dm} dataSource={dataSourceKey}>
|
||||
<AssociationOrCollectionProvider
|
||||
@ -819,7 +819,7 @@ export const SchemaSettingsModalItem: FC<SchemaSettingsModalItemProps> = (props)
|
||||
</AssociationOrCollectionProvider>
|
||||
</DataSourceApplicationProvider>
|
||||
</BlockRequestContext_deprecated.Provider>
|
||||
</Router>
|
||||
</LocationSearchContext.Provider>
|
||||
</FormActiveFieldsProvider>
|
||||
</SubFormProvider>
|
||||
</FormBlockContext.Provider>
|
||||
|
@ -40,5 +40,6 @@ export const useParentPopupVariable = (props: any = {}) => {
|
||||
/** 当前记录对应的 collection name */
|
||||
collectionName: collection?.name,
|
||||
dataSource: collection?.dataSource,
|
||||
defaultValue: undefined,
|
||||
};
|
||||
};
|
||||
|
@ -40,5 +40,6 @@ export const usePopupVariable = (props: any = {}) => {
|
||||
/** 当前记录对应的 collection name */
|
||||
collectionName: collection?.name,
|
||||
dataSource: collection?.dataSource,
|
||||
defaultValue: undefined,
|
||||
};
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ import _ from 'lodash';
|
||||
import qs from 'qs';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useLocationSearch } from '../../../application/CustomRouterContextProvider';
|
||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||
import { Option } from '../type';
|
||||
import { getLabelWithTooltip } from './useBaseVariable';
|
||||
@ -64,9 +64,9 @@ export const useURLSearchParamsCtx = (search: string) => {
|
||||
export const useURLSearchParamsVariable = (props: any = {}) => {
|
||||
const variableName = '$nURLSearchParams';
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const searchString = useLocationSearch();
|
||||
const { isVariableParsedInOtherContext } = useFlag();
|
||||
const urlSearchParamsCtx = useURLSearchParamsCtx(location.search);
|
||||
const urlSearchParamsCtx = useURLSearchParamsCtx(searchString);
|
||||
const disabled = useMemo(() => _.isEmpty(urlSearchParamsCtx), [urlSearchParamsCtx]);
|
||||
const urlSearchParamsSettings: Option = useMemo(() => {
|
||||
return {
|
||||
@ -100,8 +100,7 @@ export const useURLSearchParamsVariable = (props: any = {}) => {
|
||||
/** 变量值 */
|
||||
urlSearchParamsCtx,
|
||||
/**
|
||||
* 这里是用于当通过该变量解析出来的值是一个 undefined 时,最终应该返回的值。
|
||||
* 默认返回的是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件,
|
||||
* 这里的默认值是 null,这样会导致数据范围中的 filter 条件不会被清除掉,而 URL search params 变量的值为空时,应该清除掉 filter 条件,
|
||||
* 所以这里把 defaultValue 设置为 undefined,这样在解析出来的值是 undefined 时,会返回 undefined,从而清除掉 filter 条件。
|
||||
*/
|
||||
defaultValue: undefined,
|
||||
|
@ -10,8 +10,9 @@
|
||||
import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout';
|
||||
import { Input, Spin } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
|
||||
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
|
||||
import { RemoteSchemaComponent, SchemaComponentContext } from '../schema-component';
|
||||
|
||||
const EditableTitle = (props) => {
|
||||
@ -68,7 +69,7 @@ const EditableTitle = (props) => {
|
||||
};
|
||||
|
||||
export const BlockTemplateDetails = () => {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const params = useParams<any>();
|
||||
const key = params?.key;
|
||||
const value = useContext(SchemaComponentContext);
|
||||
|
@ -13,8 +13,8 @@ import { error } from '@nocobase/utils/client';
|
||||
import { App, Dropdown, Menu, MenuProps } from 'antd';
|
||||
import React, { createContext, useCallback, useMemo as useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useACLRoleContext, useAPIClient, useCurrentUserContext, useToken } from '..';
|
||||
import { useNavigateNoUpdate } from '../application/CustomRouterContextProvider';
|
||||
import { useChangePassword } from './ChangePassword';
|
||||
import { useCurrentUserSettingsMenu } from './CurrentUserSettingsMenuProvider';
|
||||
import { useEditProfile } from './EditProfile';
|
||||
@ -48,7 +48,7 @@ export const SettingsMenu: React.FC<{
|
||||
const { redirectUrl = '' } = props;
|
||||
const { allowAll, snippets } = useACLRoleContext();
|
||||
const appAllowed = allowAll || snippets?.includes('app');
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const api = useAPIClient();
|
||||
const { t } = useTranslation();
|
||||
const silenceApi = useAPIClient();
|
||||
|
@ -8,9 +8,10 @@
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useACLRoleContext } from '../acl';
|
||||
import { ReturnTypeOfUseRequest, useRequest } from '../api-client';
|
||||
import { useLocationNoUpdate } from '../application';
|
||||
import { useAppSpin } from '../application/hooks/useAppSpin';
|
||||
import { useCompile } from '../schema-component';
|
||||
|
||||
@ -53,7 +54,7 @@ export const CurrentUserProvider = (props) => {
|
||||
|
||||
export const NavigateIfNotSignIn = ({ children }) => {
|
||||
const result = useCurrentUserContext();
|
||||
const { pathname, search } = useLocation();
|
||||
const { pathname, search } = useLocationNoUpdate();
|
||||
const redirect = `?redirect=${pathname}${search}`;
|
||||
if (!result?.data?.data?.id) {
|
||||
return <Navigate replace to={`/signin${redirect}`} />;
|
||||
|
@ -37,11 +37,13 @@ const useLocalVariables = (props?: Props) => {
|
||||
popupRecordCtx,
|
||||
collectionName: collectionNameOfPopupRecord,
|
||||
dataSource: popupDataSource,
|
||||
defaultValue: defaultValueOfPopupRecord,
|
||||
} = usePopupVariable();
|
||||
const {
|
||||
parentPopupRecordCtx,
|
||||
collectionName: collectionNameOfParentPopupRecord,
|
||||
dataSource: parentPopupDataSource,
|
||||
defaultValue: defaultValueOfParentPopupRecord,
|
||||
} = useParentPopupVariable();
|
||||
const { datetimeCtx } = useDatetimeVariable();
|
||||
const { currentFormCtx } = useCurrentFormVariable({ form: props?.currentForm });
|
||||
@ -98,12 +100,14 @@ const useLocalVariables = (props?: Props) => {
|
||||
ctx: popupRecordCtx,
|
||||
collectionName: collectionNameOfPopupRecord,
|
||||
dataSource: popupDataSource,
|
||||
defaultValue: defaultValueOfPopupRecord,
|
||||
},
|
||||
{
|
||||
name: '$nParentPopupRecord',
|
||||
ctx: parentPopupRecordCtx,
|
||||
collectionName: collectionNameOfParentPopupRecord,
|
||||
dataSource: parentPopupDataSource,
|
||||
defaultValue: defaultValueOfParentPopupRecord,
|
||||
},
|
||||
{
|
||||
name: '$nForm',
|
||||
@ -138,12 +142,15 @@ const useLocalVariables = (props?: Props) => {
|
||||
collectionNameOfParentRecord,
|
||||
currentParentRecordDataSource,
|
||||
popupRecordCtx,
|
||||
parentPopupRecordCtx,
|
||||
collectionNameOfPopupRecord,
|
||||
popupDataSource,
|
||||
datetimeCtx,
|
||||
shouldDisplayCurrentObject,
|
||||
currentObjectCtx,
|
||||
currentCollectionName,
|
||||
defaultValueOfPopupRecord,
|
||||
defaultValueOfParentPopupRecord,
|
||||
]); // 尽量保持返回的值不变,这样可以减少接口的请求次数,因为关系字段会缓存到变量的 ctx 中
|
||||
};
|
||||
|
||||
|
@ -347,10 +347,14 @@ export class APIClient {
|
||||
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 handler = {
|
||||
get: (_: any, actionName: string) => {
|
||||
if (cancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = name.split('.').join(`/${encodeURIComponent(of) || '_'}/`);
|
||||
url += `:${actionName}`;
|
||||
const config: AxiosRequestConfig = { url };
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
useCollectionManager_deprecated,
|
||||
useCollection_deprecated,
|
||||
useCompile,
|
||||
useNavigateNoUpdate,
|
||||
useRemoveGridFormItem,
|
||||
useTableBlockContext,
|
||||
} from '@nocobase/client';
|
||||
@ -23,7 +24,6 @@ import { isURL } from '@nocobase/utils/client';
|
||||
import { App, message } from 'antd';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const useCustomBulkEditFormItemInitializerFields = (options?: any) => {
|
||||
const { name, fields } = useCollection_deprecated();
|
||||
@ -83,7 +83,7 @@ export const useCustomizeBulkEditActionProps = () => {
|
||||
const { field, resource, __parent } = useBlockRequestContext();
|
||||
const expressionScope = useContext(SchemaExpressionScopeContext);
|
||||
const actionContext = useActionContext();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const compile = useCompile();
|
||||
const actionField = useField();
|
||||
const tableBlockContext = useTableBlockContext();
|
||||
|
@ -16,13 +16,13 @@ import {
|
||||
useCollection_deprecated,
|
||||
useCompile,
|
||||
useLocalVariables,
|
||||
useNavigateNoUpdate,
|
||||
useTableBlockContext,
|
||||
useVariables,
|
||||
} from '@nocobase/client';
|
||||
import { isURL } from '@nocobase/utils/client';
|
||||
import { App, message } from 'antd';
|
||||
import { useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBulkUpdateTranslation } from './locale';
|
||||
|
||||
export const useCustomizeBulkUpdateActionProps = () => {
|
||||
@ -32,7 +32,7 @@ export const useCustomizeBulkUpdateActionProps = () => {
|
||||
const tableBlockContext = useTableBlockContext();
|
||||
const { rowKey } = tableBlockContext;
|
||||
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const compile = useCompile();
|
||||
const { t } = useBulkUpdateTranslation();
|
||||
const actionField: any = useField();
|
||||
|
@ -8,15 +8,21 @@
|
||||
*/
|
||||
|
||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||
import { useAPIClient, useActionContext, useCompile, useDataSourceKey, useRecord } from '@nocobase/client';
|
||||
import {
|
||||
useAPIClient,
|
||||
useActionContext,
|
||||
useCompile,
|
||||
useDataSourceKey,
|
||||
useNavigateNoUpdate,
|
||||
useRecord,
|
||||
} from '@nocobase/client';
|
||||
import { isURL } from '@nocobase/utils/client';
|
||||
import { App } from 'antd';
|
||||
import { saveAs } from 'file-saver';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const useCustomizeRequestActionProps = () => {
|
||||
const apiClient = useAPIClient();
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigateNoUpdate();
|
||||
const actionSchema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
const form = useForm();
|
||||
|
@ -12,16 +12,16 @@ import { ISchema, connect, mapProps, useField, useFieldSchema, useForm } from '@
|
||||
import {
|
||||
ActionDesigner,
|
||||
SchemaSettingOpenModeSchemaItems,
|
||||
useCollection_deprecated,
|
||||
useRecord,
|
||||
SchemaSettingsModalItem,
|
||||
SchemaSettings,
|
||||
SchemaSettingsItemType,
|
||||
SchemaSettingsLinkageRules,
|
||||
SchemaSettingsModalItem,
|
||||
useCollectionState,
|
||||
useCollection_deprecated,
|
||||
useDesignable,
|
||||
useRecord,
|
||||
useSchemaToolbar,
|
||||
useSyncFromForm,
|
||||
SchemaSettings,
|
||||
} from '@nocobase/client';
|
||||
import { Tree as AntdTree } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
@ -357,19 +357,19 @@ const schemaSettingsItems: SchemaSettingsItemType[] = [
|
||||
name: 'openMode',
|
||||
Component: SchemaSettingOpenModeSchemaItems,
|
||||
useComponentProps() {
|
||||
const fieldSchema = useFieldSchema();
|
||||
const isPopupAction = [
|
||||
'create',
|
||||
'update',
|
||||
'view',
|
||||
'customize:popup',
|
||||
'duplicate',
|
||||
'customize:create',
|
||||
].includes(fieldSchema['x-action'] || '');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const modeOptions = useMemo(() => {
|
||||
return [
|
||||
{ label: t('Drawer'), value: 'drawer' },
|
||||
{ label: t('Dialog'), value: 'modal' },
|
||||
];
|
||||
}, [t]);
|
||||
|
||||
return {
|
||||
openMode: isPopupAction,
|
||||
openSize: isPopupAction,
|
||||
openMode: true,
|
||||
openSize: true,
|
||||
modeOptions,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -7,16 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { useApp } from '@nocobase/client';
|
||||
import { useApp, useLocationSearch } from '@nocobase/client';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const AuthProvider: React.FC = (props) => {
|
||||
const location = useLocation();
|
||||
const searchString = useLocationSearch();
|
||||
const app = useApp();
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const params = new URLSearchParams(searchString);
|
||||
const authenticator = params.get('authenticator');
|
||||
const token = params.get('token');
|
||||
if (token) {
|
||||
|
@ -6,10 +6,10 @@
|
||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
import { LeftOutlined, FileImageOutlined } from '@ant-design/icons';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { FileImageOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import { useActionContext } from '@nocobase/client';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScanBox } from './ScanBox';
|
||||
import { useScanner } from './useScanner';
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user