Merge remote-tracking branch 'upstream/main' into @hera/dev

This commit is contained in:
sealday 2024-03-29 17:51:25 +08:00
commit fd656387ea
134 changed files with 2906 additions and 739 deletions

View File

@ -17,6 +17,7 @@ services:
- DB_USER=nocobase
- DB_PASSWORD=nocobase
- DB_TIMEZONE=+08:00
- DB_UNDERSCORED=true
volumes:
- ./storage:/app/nocobase/storage
ports:
@ -29,5 +30,7 @@ services:
MYSQL_PASSWORD: nocobase
MYSQL_ROOT_PASSWORD: nocobase
restart: always
volumes:
- ./storage/db/mariadb:/var/lib/mysql
networks:
- nocobase

View File

@ -29,5 +29,7 @@ services:
MYSQL_PASSWORD: nocobase
MYSQL_ROOT_PASSWORD: nocobase
restart: always
volumes:
- ./storage/db/mysql:/var/lib/mysql
networks:
- nocobase

View File

@ -28,5 +28,7 @@ services:
POSTGRES_USER: nocobase
POSTGRES_DB: nocobase
POSTGRES_PASSWORD: nocobase
volumes:
- ./storage/db/postgres:/var/lib/postgresql/data
networks:
- nocobase

View File

@ -18,6 +18,7 @@
"scripts": {
"nocobase": "nocobase",
"pm": "nocobase pm",
"pm2": "nocobase pm2",
"dev": "nocobase dev",
"dev-server": "nocobase dev --server",
"start": "nocobase start",

View File

@ -9,6 +9,7 @@ import {
getCjsPackages,
getPresetsPackages,
ROOT_PATH,
ESM_PACKAGES,
} from './constant';
import { buildClient } from './buildClient';
import { buildCjs } from './buildCjs';
@ -18,6 +19,7 @@ import { PkgLog, getPkgLog, toUnixPath, getPackageJson, getUserConfig, UserConfi
import { getPackages } from './utils/getPackages';
import { Package } from '@lerna/package';
import { tarPlugin } from './tarPlugin'
import { buildEsm } from './buildEsm';
const BUILD_ERROR = 'build-error';
@ -51,6 +53,8 @@ export async function build(pkgs: string[]) {
if (clientCore) {
await buildPackage(clientCore, 'es', buildClient);
}
const esmPackages = cjsPackages.filter(pkg => ESM_PACKAGES.includes(pkg.name));
await buildPackages(esmPackages, 'es', buildEsm);
// plugins/*、samples/*
await buildPackages(pluginPackages, 'dist', buildPlugin);

View File

@ -19,14 +19,14 @@ export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap
}
return true;
};
await buildEsm(cwd, userConfig, sourcemap, external, log);
await buildLib(cwd, userConfig, sourcemap, external, log);
await buildClientEsm(cwd, userConfig, sourcemap, external, log);
await buildClientLib(cwd, userConfig, sourcemap, external, log);
await buildLocale(cwd, userConfig, log);
}
type External = (id: string) => boolean;
export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean, external: External, log: PkgLog) {
function buildClientEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean, external: External, log: PkgLog) {
log('build client esm');
const entry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
const outDir = path.resolve(cwd, 'es');
@ -61,7 +61,7 @@ export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean
);
}
export async function buildLib(
async function buildClientLib(
cwd: string,
userConfig: UserConfig,
sourcemap: boolean,

View File

@ -0,0 +1,66 @@
import path from 'path';
import { PkgLog, UserConfig } from './utils';
import { build as viteBuild } from 'vite';
import fg from 'fast-glob';
export async function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
log('build esm');
const indexEntry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
const outDir = path.resolve(cwd, 'es');
await build(cwd, indexEntry, outDir, userConfig, sourcemap, log);
const clientEntry = fg.sync(['src/client/index.ts', 'src/client.ts'], { cwd, absolute: true, onlyFiles: true })?.[0]?.replaceAll(/\\/g, '/');
const clientOutDir = path.resolve(cwd, 'es/client');
if (clientEntry) {
await build(cwd, clientEntry, clientOutDir, userConfig, sourcemap, log);
}
const pkg = require(path.join(cwd, 'package.json'));
if (pkg.name === '@nocobase/test') {
const e2eEntry = path.join(cwd, 'src/e2e/index.ts').replaceAll(/\\/g, '/');
const e2eOutDir = path.resolve(cwd, 'es/e2e');
await build(cwd, e2eEntry, e2eOutDir, userConfig, sourcemap, log);
}
}
function build(cwd: string, entry: string, outDir: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
const cwdWin = cwd.replaceAll(/\\/g, '/');
const cwdUnix = cwd.replaceAll(/\//g, '\\');
const external = function (id: string) {
if (id.startsWith('.') || id.startsWith(cwdUnix) || id.startsWith(cwdWin)) {
return false;
}
return true;
};
return viteBuild(
userConfig.modifyViteConfig({
mode: process.env.NODE_ENV || 'production',
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
'process.env.__TEST__': false,
'process.env.__E2E__': process.env.__E2E__ ? true : false,
},
build: {
minify: false,
outDir,
cssCodeSplit: true,
emptyOutDir: true,
sourcemap,
lib: {
entry,
formats: ['es'],
fileName: 'index',
},
target: ['node16'],
rollupOptions: {
cache: true,
treeshake: true,
external,
},
},
}),
);
}

View File

@ -39,6 +39,7 @@ export const getPresetsPackages = (packages: Package[]) =>
packages.filter((item) => item.location.startsWith(PRESETS_DIR));
export const CORE_APP = path.join(PACKAGES_PATH, 'core/app');
export const CORE_CLIENT = path.join(PACKAGES_PATH, 'core/client');
export const ESM_PACKAGES = ['@nocobase/test'];
export const CJS_EXCLUDE_PACKAGES = [
path.join(PACKAGES_PATH, 'core/build'),
path.join(PACKAGES_PATH, 'core/cli'),

View File

@ -16,6 +16,7 @@ module.exports = (cli) => {
require('./e2e')(cli);
require('./clean')(cli);
require('./doc')(cli);
require('./pm2')(cli);
require('./test')(cli);
require('./umi')(cli);
require('./upgrade')(cli);

View File

@ -0,0 +1,28 @@
const chalk = require('chalk');
const { Command } = require('commander');
const { run, isDev } = require('../util');
/**
*
* @param {Command} cli
*/
module.exports = (cli) => {
cli
.command('pm2')
.allowUnknownOption()
.action(() => {
run('pm2', process.argv.slice(3));
});
cli
.command('pm2-restart')
.allowUnknownOption()
.action(() => {
run('pm2', ['restart', 'all']);
});
cli
.command('pm2-stop')
.allowUnknownOption()
.action(() => {
run('pm2', ['stop', 'all']);
});
};

View File

@ -175,6 +175,9 @@ export class Application {
return this.dataSourceManager.getDataSource(dataSource)?.collectionManager;
}
/**
* @internal
*/
getComposeProviders() {
const Providers = compose(...this.providers)(BlankComponent);
Providers.displayName = 'Providers';
@ -280,6 +283,9 @@ export class Application {
return React.createElement(this.getComponent(Component), props);
}
/**
* @internal use addComponents({ SomeComponent }) instead
*/
protected addComponent(component: ComponentType, name?: string) {
const componentName = name || component.displayName || component.name;
if (!componentName) {

View File

@ -26,6 +26,9 @@ export class PluginManager {
this.initPlugins = this.init(_plugins);
}
/**
* @internal
*/
async init(_plugins: PluginType[]) {
await this.initStaticPlugins(_plugins);
if (this.loadRemotePlugins) {
@ -84,6 +87,9 @@ export class PluginManager {
return new plugin(opts, this.app);
}
/**
* @internal
*/
async load() {
await this.initPlugins;

View File

@ -41,6 +41,9 @@ export class RouterManager {
this.app = app;
}
/**
* @internal
*/
getRoutesTree(): RouteObject[] {
type RouteTypeWithChildren = RouteType & { children?: RouteTypeWithChildren };
const routes: Record<string, RouteTypeWithChildren> = {};
@ -106,6 +109,9 @@ export class RouterManager {
this.options.basename = basename;
}
/**
* @internal
*/
getRouterComponent() {
const { type = 'browser', ...opts } = this.options;
const Routers = {

View File

@ -9,11 +9,17 @@ import { SchemaInitializerOptions } from '../types';
export * from './useAriaAttributeOfMenuItem';
/**
* @internal
*/
export function useSchemaInitializerMenuItems(items: any[], name?: string, onClick?: (args: any) => void) {
const getMenuItems = useGetSchemaInitializerMenuItems(onClick);
return useMemo(() => getMenuItems(items, name), [getMenuItems, items, name]);
}
/**
* @internal
*/
export function useGetSchemaInitializerMenuItems(onClick?: (args: any) => void) {
const compile = useCompile();

View File

@ -1,6 +1,9 @@
import { useMemo } from 'react';
import { useSchemaInitializerSubMenuContext } from '../components/SchemaInitializerSubMenu';
/**
* @internal
*/
export const useAriaAttributeOfMenuItem = () => {
const { isInMenu } = useSchemaInitializerSubMenuContext();
// 在 Menu 中,每一项的 role 已经被标记为 menuitem 了,不需要再次标记;

View File

@ -7,6 +7,9 @@ import React from 'react';
import { useDesignable } from '../../../schema-component';
import { useField, useFieldSchema } from '@formily/react';
/**
* @internal
*/
export const SchemaSettingsWrapper: FC<SchemaSettingOptions<any>> = (props) => {
const { items, Component = SchemaSettingsIcon, name, componentProps, style, ...others } = props;
const { dn } = useDesignable();

View File

@ -35,6 +35,9 @@ import * as nocobaseClient from '../../index';
import type { RequireJS } from './requirejs';
/**
* @internal
*/
export function defineGlobalDeps(requirejs: RequireJS) {
// react
requirejs.define('react', () => React);

View File

@ -3,12 +3,18 @@ import type { PluginData } from '../PluginManager';
import type { RequireJS } from './requirejs';
import type { DevDynamicImport } from '../Application';
/**
* @internal
*/
export function defineDevPlugins(plugins: Record<string, typeof Plugin>) {
Object.entries(plugins).forEach(([packageName, plugin]) => {
window.define(`${packageName}/client`, () => plugin);
});
}
/**
* @internal
*/
export function definePluginClient(packageName: string) {
window.define(`${packageName}/client`, ['exports', packageName], function (_exports: any, _pluginExports: any) {
Object.defineProperty(_exports, '__esModule', {
@ -27,6 +33,9 @@ export function definePluginClient(packageName: string) {
});
}
/**
* @internal
*/
export function configRequirejs(requirejs: any, pluginData: PluginData[]) {
requirejs.requirejs.config({
waitSeconds: 120,
@ -37,6 +46,9 @@ export function configRequirejs(requirejs: any, pluginData: PluginData[]) {
});
}
/**
* @internal
*/
export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins: [string, typeof Plugin][]) => void) {
return (...pluginModules: (typeof Plugin & { default?: typeof Plugin })[]) => {
const res: [string, typeof Plugin][] = pluginModules
@ -58,6 +70,9 @@ export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins
};
}
/**
* @internal
*/
export function getRemotePlugins(
requirejs: any,
pluginData: PluginData[] = [],
@ -80,6 +95,9 @@ interface GetPluginsOption {
devDynamicImport?: DevDynamicImport;
}
/**
* @internal
*/
export async function getPlugins(options: GetPluginsOption): Promise<Array<[string, typeof Plugin]>> {
const { requirejs, pluginData, devDynamicImport } = options;
if (pluginData.length === 0) return [];

View File

@ -10,6 +10,9 @@ export interface RequireJS {
define: RequireDefine
}
/**
* @internal
*/
export function getRequireJs(): RequireJS {
var requirejs, require, define;
var req, s, head, baseElement, dataMain, src,

View File

@ -24,6 +24,13 @@ import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table
import { useListBlockDecoratorProps } from '../modules/blocks/data-blocks/list/hooks/useListBlockDecoratorProps';
import { useTableSelectorDecoratorProps } from '../modules/blocks/data-blocks/table-selector/hooks/useTableSelectorDecoratorProps';
import { useCollapseBlockDecoratorProps } from '../modules/blocks/filter-blocks/collapse/hooks/useCollapseBlockDecoratorProps';
import { useFilterFormBlockProps } from '../modules/blocks/filter-blocks/form/hooks/useFilterFormBlockProps';
import { useFilterFormBlockDecoratorProps } from '../modules/blocks/filter-blocks/form/hooks/useFilterFormBlockDecoratorProps';
import { useGridCardBlockDecoratorProps } from '../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockDecoratorProps';
import { useDetailsProps } from '../modules/blocks/data-blocks/details-single/hooks/useDetailsProps';
import { useDetailsWithPaginationProps } from '../modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationProps';
import { useDetailsDecoratorProps } from '../modules/blocks/data-blocks/details-single/hooks/useDetailsDecoratorProps';
import { useDetailsWithPaginationDecoratorProps } from '../modules/blocks/data-blocks/details-multi/hooks/useDetailsWithPaginationDecoratorProps';
// TODO: delete this, replaced by `BlockSchemaComponentPlugin`
export const BlockSchemaComponentProvider: React.FC = (props) => {
@ -51,6 +58,10 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useEditFormBlockProps,
useFormFieldProps,
useDetailsBlockProps,
useDetailsProps,
useDetailsWithPaginationProps,
useDetailsDecoratorProps,
useDetailsWithPaginationDecoratorProps,
useTableFieldProps,
useTableBlockProps,
useTableSelectorProps,
@ -58,6 +69,9 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useListBlockDecoratorProps,
useTableSelectorDecoratorProps,
useCollapseBlockDecoratorProps,
useFilterFormBlockProps,
useFilterFormBlockDecoratorProps,
useGridCardBlockDecoratorProps,
}}
>
{props.children}
@ -102,6 +116,10 @@ export class BlockSchemaComponentPlugin extends Plugin {
useEditFormBlockProps,
useFormFieldProps,
useDetailsBlockProps,
useDetailsProps,
useDetailsWithPaginationProps,
useDetailsDecoratorProps,
useDetailsWithPaginationDecoratorProps,
useTableFieldProps,
useTableBlockProps,
useTableSelectorProps,
@ -109,6 +127,9 @@ export class BlockSchemaComponentPlugin extends Plugin {
useListBlockDecoratorProps,
useTableSelectorDecoratorProps,
useCollapseBlockDecoratorProps,
useFilterFormBlockProps,
useFilterFormBlockDecoratorProps,
useGridCardBlockDecoratorProps,
});
}
}

View File

@ -2,11 +2,12 @@ import { createForm } from '@formily/core';
import { useField } from '@formily/react';
import { Spin } from 'antd';
import _ from 'lodash';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import React, { createContext, useContext, useEffect, useMemo, useRef } from 'react';
import { useCollectionParentRecord } from '../data-source/collection-record/CollectionRecordProvider';
import { RecordProvider } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { useParsedFilter } from './hooks';
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
export const DetailsBlockContext = createContext<any>({});
DetailsBlockContext.displayName = 'DetailsBlockContext';
@ -24,6 +25,7 @@ const InternalDetailsBlockProvider = (props) => {
const { resource, service } = useBlockRequestContext();
const parentRecord = useCollectionParentRecord();
const currentRecord = (action === 'list' ? service?.data?.data?.[0] : service?.data?.data) || {};
const formBlockRef = useRef();
const detailsBLockValue = useMemo(() => {
return {
action,
@ -31,6 +33,7 @@ const InternalDetailsBlockProvider = (props) => {
field,
service,
resource,
formBlockRef,
};
}, [action, field, form, resource, service]);
@ -50,20 +53,22 @@ const InternalDetailsBlockProvider = (props) => {
return (
<DetailsBlockContext.Provider value={detailsBLockValue}>
<div ref={formBlockRef}>
<RecordProvider isNew={false} record={currentRecord} parent={parentRecord?.data}>
{props.children}
</RecordProvider>
</div>
</DetailsBlockContext.Provider>
);
};
export const DetailsBlockProvider = (props) => {
export const DetailsBlockProvider = withDynamicSchemaProps((props) => {
return (
<BlockProvider name="details" {...props}>
<InternalDetailsBlockProvider {...props} />
</BlockProvider>
);
};
});
/**
* @deprecated
@ -72,6 +77,11 @@ export const useDetailsBlockContext = () => {
return useContext(DetailsBlockContext);
};
/**
* @deprecated
* useDetailsWithPaginationProps useDetailsProps
* @returns
*/
export const useDetailsBlockProps = () => {
const ctx = useDetailsBlockContext();
useEffect(() => {

View File

@ -2,8 +2,9 @@ import React from 'react';
import { DatePickerProvider } from '../schema-component';
import { DefaultValueProvider } from '../schema-settings';
import { FormBlockProvider } from './FormBlockProvider';
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
export const FilterFormBlockProvider = (props) => {
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
return (
<DatePickerProvider value={{ utc: false }}>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
@ -11,4 +12,4 @@ export const FilterFormBlockProvider = (props) => {
</DefaultValueProvider>
</DatePickerProvider>
);
};
});

View File

@ -6,4 +6,5 @@ export * from './FormFieldProvider';
export * from './TableBlockProvider';
export * from './TableFieldProvider';
export * from './TableSelectorProvider';
export * from './DetailsBlockProvider';
export * from './hooks';

View File

@ -17,7 +17,7 @@ export class UUIDFieldInterface extends CollectionFieldInterface {
'x-validator': 'uuid',
},
};
availableTypes = ['string', 'uid'];
availableTypes = ['string', 'uid', 'uuid'];
properties = {
'uiSchema.title': {
type: 'string',

View File

@ -110,7 +110,7 @@ export class InheritanceCollectionMixin extends Collection {
}
// override Collection
getFieldsMap() {
protected getFieldsMap() {
if (this.fieldsMap) {
return this.fieldsMap;
}

View File

@ -2,19 +2,7 @@ import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { useIsFileField } from '../schema-component/antd/form-item/FormItem.Settings';
import { useColumnSchema } from '../schema-component/antd/table-v2/Table.Column.Decorator';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../collection-manager';
/**
*
* @returns
*/
function useCollectionField() {
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return collectionField;
}
import { useCollectionField } from '../data-source';
export function useFieldComponentName(): string {
const { fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema();

View File

@ -13,7 +13,10 @@ type Props = {
children?: React.ReactNode;
};
// TODO: 初步适配
/**
* TODO: 初步适配
* @internal
*/
export const CollectionFieldInternalField: React.FC = (props: Props) => {
const { component } = props;
const compile = useCompile();

View File

@ -22,7 +22,7 @@ export const CollectionFieldProvider: FC<CollectionFieldProviderProps> = (props)
const collectionManager = useCollectionManager();
const value = useMemo(() => {
if (!collection || allowNull) return null;
if (!collection) return null;
const field = fieldSchema?.['x-component-props']?.['field'];
return (
collectionManager.getCollectionField(fieldSchema?.['x-collection-field']) ||
@ -31,7 +31,7 @@ export const CollectionFieldProvider: FC<CollectionFieldProviderProps> = (props)
);
}, [collection, fieldSchema, name, collectionManager]);
if (!collection || allowNull) {
if (!value && allowNull) {
return <>{children}</>;
}

View File

@ -234,7 +234,8 @@ export class Collection {
getFields(predicate?: GetCollectionFieldPredicate) {
return predicate ? filter(this.fields, predicate) : this.fields;
}
getFieldsMap() {
protected getFieldsMap() {
if (!this.fieldsMap) {
this.fieldsMap = this.getFields().reduce((memo, field) => {
memo[field.name] = field;

View File

@ -131,6 +131,9 @@ export class CollectionManager {
return this.getCollection(collectionName)?.getFields(predicate) || [];
}
/**
* @internal
*/
clone(collections: CollectionOptions[] = []) {
const collectionManager = new CollectionManager([], this.dataSource);

View File

@ -1,6 +1,9 @@
import type { Application } from '../../application/Application';
import type { CollectionOptions } from './Collection';
/**
* @internal
*/
export const collectionTransform = (collection: CollectionOptions, app: Application) => {
const { rawTitle, title, fields = [], ...rest } = collection;
return {
@ -26,6 +29,9 @@ export const collectionTransform = (collection: CollectionOptions, app: Applicat
};
};
/**
* @internal
*/
export function applyMixins(Cls: any, mixins: any[], options?: any, collectionManager?: any) {
const instance = new Cls(options, collectionManager);
mixins.forEach((MixinClass) => {

View File

@ -1,7 +1,7 @@
import { App, Button, Result, Typography } from 'antd';
import React, { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CardItem, useCompile, useDesignable } from '../../schema-component';
import { CardItem, EllipsisWithTooltip, useCompile, useDesignable } from '../../schema-component';
import { useDataSource } from '../data-source/DataSourceProvider';
import { useDataSourceManager } from '../data-source';
import { DEFAULT_DATA_SOURCE_KEY } from '../../data-source/data-source/DataSourceManager';
@ -15,6 +15,9 @@ export interface CollectionDeletedPlaceholderProps {
const { Text } = Typography;
/**
* @internal
*/
export const CollectionDeletedPlaceholder: FC<CollectionDeletedPlaceholderProps> = ({ type, name, message }) => {
const { designable, dn } = useDesignable();
const { modal } = App.useApp();
@ -63,7 +66,11 @@ export const CollectionDeletedPlaceholder: FC<CollectionDeletedPlaceholderProps>
if (designable) {
if (type === 'Field') {
return <Text type="secondary">{messageValue}</Text>;
return (
<EllipsisWithTooltip ellipsis>
<Text type="secondary">{messageValue}</Text>
</EllipsisWithTooltip>
);
}
return (

View File

@ -10,6 +10,9 @@ interface DataSourceApplicationProviderProps extends CollectionManagerProviderPr
dataSourceManager: DataSourceManager;
}
/**
* @internal
*/
export const DataSourceApplicationProvider: FC<DataSourceApplicationProviderProps> = ({
children,
dataSourceManager,

View File

@ -103,6 +103,9 @@ export interface DataBlockContextValue<T extends {} = {}> {
export const DataBlockContext = createContext<DataBlockContextValue<any>>({} as any);
DataBlockContext.displayName = 'DataBlockContext';
/**
* @internal
*/
export const AssociationOrCollectionProvider = (props: {
collection: string | CollectionOptions;
association: string;

View File

@ -7,6 +7,7 @@ import { AllDataBlockProps, useDataBlockProps } from './DataBlockProvider';
import { useDataBlockResource } from './DataBlockResourceProvider';
import { useDataSourceHeaders } from '../utils';
import { useDataLoadingMode } from '../../modules/blocks/data-blocks/details-multi/setDataLoadingModeSettingsItem';
import { useCollection, useCollectionManager } from '../collection';
export const BlockRequestContext = createContext<UseRequestResult<any>>(null);
BlockRequestContext.displayName = 'BlockRequestContext';
@ -55,6 +56,7 @@ function useCurrentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
function useParentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
const { sourceId, association, parentRecord } = options;
const api = useAPIClient();
const cm = useCollectionManager();
const dataBlockProps = useDataBlockProps();
const headers = useDataSourceHeaders(dataBlockProps.dataSource);
return useRequest<T>(
@ -63,8 +65,12 @@ function useParentRequest<T>(options: Omit<AllDataBlockProps, 'type'>) {
if (!association) return Promise.resolve({ data: undefined });
// "association": "Collection.Field"
const arr = association.split('.');
// <collection>:get/<filterByTk>
const url = `${arr[0]}:get/${sourceId}`;
const field = cm.getCollectionField(association);
const isM2O = field.interface === 'm2o';
const filterTargetKey = cm.getCollection(arr[0]).getOption('filterTargetKey');
const filterKey = isM2O ? filterTargetKey : field.sourceKey;
// <collection>:get?filter[filterKey]=sourceId
const url = `${arr[0]}:get?filter[${filterKey}]=${sourceId}`;
const res = await api.request({ url, headers });
return res.data;
},
@ -88,6 +94,7 @@ export const BlockRequestProvider: FC = ({ children }) => {
requestOptions,
requestService,
} = props;
const currentRequest = useCurrentRequest<{ data: any }>({
action,
sourceId,

View File

@ -83,6 +83,9 @@ export abstract class DataSource {
}
}
/**
* @internal
*/
export class LocalDataSource extends DataSource {
getDataSource() {
return {

View File

@ -53,6 +53,10 @@ export const detailsActionInitializers_deprecated = new CompatibleSchemaInitiali
],
});
/**
* @deprecated
* 使 readPrettyFormActionInitializers
*/
export const detailsActionInitializers = new CompatibleSchemaInitializer(
{
name: 'detailsWithPaging:configureActions',

View File

@ -2,9 +2,9 @@ import { TableOutlined } from '@ant-design/icons';
import React from 'react';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { useCollectionManager_deprecated } from '../../../../collection-manager';
import { createDetailsBlockSchema } from '../../../../schema-initializer/utils';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection';
import { createDetailsWithPaginationUISchema } from './createDetailsWithPaginationUISchema';
export const DetailsBlockInitializer = ({
filterCollections,
@ -51,15 +51,14 @@ export const DetailsBlockInitializer = ({
const { item } = options;
const collection = getCollection(item.name, item.dataSource);
const schema = createDetailsBlockSchema({
collection: item.name,
const schema = createDetailsWithPaginationUISchema({
collectionName: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
actionInitializers:
hideActionInitializer: !(
(collection.template !== 'view' || collection?.writableView) &&
collection.template !== 'sql' &&
'detailsWithPaging:configureActions',
settings: 'blockSettings:multiDataDetails',
collection.template !== 'sql'
),
});
insert(schema);
}}

View File

@ -0,0 +1,69 @@
import { createDetailsWithPaginationUISchema } from '../createDetailsWithPaginationUISchema';
vi.mock('@formily/shared', () => ({
uid: vi.fn().mockReturnValue('mocked-uid'),
}));
describe('createDetailsBlockWithPaginationUISchema', () => {
it('should return the correct schema', () => {
const schema = createDetailsWithPaginationUISchema({
collectionName: 'users',
dataSource: 'users',
rowKey: 'id',
});
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"mocked-uid": {
"properties": {
"grid": {
"properties": {},
"type": "void",
"x-component": "Grid",
"x-initializer": "details:configureFields",
},
"mocked-uid": {
"properties": {},
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": 24,
},
},
"x-initializer": "details:configureActions",
},
"pagination": {
"type": "void",
"x-component": "Pagination",
"x-use-component-props": "useDetailsPaginationProps",
},
},
"type": "void",
"x-component": "Details",
"x-read-pretty": true,
"x-use-component-props": "useDetailsWithPaginationProps",
},
},
"type": "void",
"x-acl-action": "users:view",
"x-component": "CardItem",
"x-decorator": "DetailsBlockProvider",
"x-decorator-props": {
"action": "list",
"association": undefined,
"collection": "users",
"dataSource": "users",
"params": {
"pageSize": 1,
},
"readPretty": true,
},
"x-settings": "blockSettings:detailsWithPagination",
"x-toolbar": "BlockSchemaToolbar",
"x-use-decorator-props": "useDetailsWithPaginationDecoratorProps",
}
`);
});
});

View File

@ -0,0 +1,70 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export function createDetailsWithPaginationUISchema(options: {
dataSource: string;
rowKey: string;
collectionName?: string;
association?: string;
templateSchema?: ISchema;
hideActionInitializer?: boolean;
}): ISchema {
const { collectionName, dataSource, association, templateSchema, hideActionInitializer, rowKey } = options;
const resourceName = association || collectionName;
if (!dataSource || !rowKey) {
throw new Error('dataSource and rowKey are required');
}
return {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsWithPaginationDecoratorProps',
'x-decorator-props': {
dataSource,
collection: collectionName,
association,
readPretty: true,
action: 'list',
params: {
pageSize: 1,
},
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:detailsWithPagination',
'x-component': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsWithPaginationProps',
properties: {
[uid()]: {
type: 'void',
'x-initializer': hideActionInitializer ? undefined : 'details:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
properties: {},
},
grid: templateSchema || {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
properties: {},
},
pagination: {
type: 'void',
'x-component': 'Pagination',
'x-use-component-props': 'useDetailsPaginationProps',
},
},
},
},
};
}

View File

@ -0,0 +1,216 @@
import { ArrayItems } from '@formily/antd-v5';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider';
import { useDetailsBlockContext } from '../../../../block-provider/DetailsBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../../collection-manager';
import { removeNullCondition, useDesignable } from '../../../../schema-component';
import { SchemaSettingsBlockTitleItem, SchemaSettingsTemplate } from '../../../../schema-settings';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { setDataLoadingModeSettingsItem, useDataLoadingMode } from './setDataLoadingModeSettingsItem';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
const commonItems: SchemaSettingsItemType[] = [
{
name: 'title',
Component: SchemaSettingsBlockTitleItem,
},
{
name: 'dataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const dataLoadingMode = useDataLoadingMode();
return {
collectionName: name,
defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {},
form,
onSubmit: ({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};
params.filter = filter;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
if (dataLoadingMode === 'auto') {
service.run({ ...service.params?.[0], filter });
}
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
},
};
},
},
{
name: 'sortingRules',
type: 'modal',
useComponentProps() {
const { name } = useCollection_deprecated();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return {
title: t('Set default sorting rules'),
components: {
ArrayItems,
},
schema: {
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema,
onSubmit({ sort }) {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
const params = field.decoratorProps.params || {};
params.sort = sortArr;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
service.run({ ...service.params?.[0], sort: sortArr });
},
};
},
},
setDataLoadingModeSettingsItem,
{
name: 'template',
Component: SchemaSettingsTemplate,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return {
componentName: 'Details',
collectionName: name,
resourceName: defaultResource,
};
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'remove',
type: 'remove',
componentProps: {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
},
},
];
/**
* @deprecated
* 使 detailsWithPaginationSettings
*/
export const multiDataDetailsBlockSettings = new SchemaSettings({
name: 'blockSettings:multiDataDetails',
items: commonItems,
});
export const detailsWithPaginationSettings = new SchemaSettings({
name: 'blockSettings:detailsWithPagination',
items: commonItems,
});

View File

@ -0,0 +1,15 @@
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
export function useDetailsWithPaginationDecoratorProps(props) {
let sourceId;
// association 的值是固定不变的,所以可以在条件中使用 hooks
if (props.association) {
// eslint-disable-next-line react-hooks/rules-of-hooks
sourceId = useDataBlockSourceId({ association: props.association });
}
return {
sourceId,
};
}

View File

@ -0,0 +1,5 @@
import { useDetailsBlockProps } from '../../../../../block-provider/DetailsBlockProvider';
export function useDetailsWithPaginationProps() {
return useDetailsBlockProps();
}

View File

@ -1,204 +0,0 @@
import { ArrayItems } from '@formily/antd-v5';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useFormBlockContext } from '../../../../block-provider';
import { useDetailsBlockContext } from '../../../../block-provider/DetailsBlockProvider';
import { useCollection_deprecated, useSortFields } from '../../../../collection-manager';
import { removeNullCondition, useDesignable } from '../../../../schema-component';
import { SchemaSettingsBlockTitleItem, SchemaSettingsTemplate } from '../../../../schema-settings';
import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSettingsDataScope';
import { setDataLoadingModeSettingsItem, useDataLoadingMode } from './setDataLoadingModeSettingsItem';
export const multiDataDetailsBlockSettings = new SchemaSettings({
name: 'blockSettings:multiDataDetails',
items: [
{
name: 'title',
Component: SchemaSettingsBlockTitleItem,
},
{
name: 'dataScope',
Component: SchemaSettingsDataScope,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const dataLoadingMode = useDataLoadingMode();
return {
collectionName: name,
defaultFilter: fieldSchema?.['x-decorator-props']?.params?.filter || {},
form,
onSubmit: ({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};
params.filter = filter;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
if (dataLoadingMode === 'auto') {
service.run({ ...service.params?.[0], filter });
}
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
},
};
},
},
{
name: 'sortingRules',
type: 'modal',
useComponentProps() {
const { name } = useCollection_deprecated();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const field = useField();
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
? {
field: item.substring(1),
direction: 'desc',
}
: {
field: item,
direction: 'asc',
};
});
return {
title: t('Set default sorting rules'),
components: {
ArrayItems,
},
schema: {
type: 'object',
title: t('Set default sorting rules'),
properties: {
sort: {
type: 'array',
default: sort,
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
sort: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.SortHandle',
},
field: {
type: 'string',
enum: sortFields,
required: true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
style: {
width: 260,
},
},
},
direction: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {
optionType: 'button',
},
enum: [
{
label: t('ASC'),
value: 'asc',
},
{
label: t('DESC'),
value: 'desc',
},
],
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add sort field'),
'x-component': 'ArrayItems.Addition',
},
},
},
},
} as ISchema,
onSubmit({ sort }) {
const sortArr = sort.map((item) => {
return item.direction === 'desc' ? `-${item.field}` : item.field;
});
const params = field.decoratorProps.params || {};
params.sort = sortArr;
field.decoratorProps.params = params;
fieldSchema['x-decorator-props']['params'] = params;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-decorator-props': fieldSchema['x-decorator-props'],
},
});
service.run({ ...service.params?.[0], sort: sortArr });
},
};
},
},
setDataLoadingModeSettingsItem,
{
name: 'template',
Component: SchemaSettingsTemplate,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return {
componentName: 'Details',
collectionName: name,
resourceName: defaultResource,
};
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'remove',
type: 'remove',
componentProps: {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
},
},
],
});

View File

@ -47,6 +47,7 @@ const AssociatedFields = () => {
/**
* @deprecated
* 使 readPrettyFormItemInitializers
*/
export const readPrettyFormItemInitializers_deprecated = new CompatibleSchemaInitializer({
name: 'ReadPrettyFormItemInitializers',

View File

@ -1,10 +1,11 @@
import { FormOutlined } from '@ant-design/icons';
import React, { useCallback } from 'react';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { useBlockAssociationContext, useBlockRequestContext } from '../../../../block-provider';
import { useBlockAssociationContext } from '../../../../block-provider';
import { useCollection_deprecated } from '../../../../collection-manager';
import { useSchemaTemplateManager } from '../../../../schema-templates';
import { createDetailsBlockSchema, useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils';
import { useRecordCollectionDataSourceItems } from '../../../../schema-initializer/utils';
import { createDetailsUISchema } from './createDetailsUISchema';
export const RecordReadPrettyFormBlockInitializer = () => {
const itemConfig = useSchemaInitializerItem();
@ -24,29 +25,27 @@ export const RecordReadPrettyFormBlockInitializer = () => {
};
export function useCreateSingleDetailsSchema() {
const itemConfig = useSchemaInitializerItem();
const { insert } = useSchemaInitializer();
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
const association = useBlockAssociationContext();
const { block } = useBlockRequestContext();
const actionInitializers =
block !== 'TableField' ? itemConfig.actionInitializers || 'details:configureActions' : null;
const templateWrap = useCallback(
(templateSchema, options) => {
const { item } = options;
if (item.template.componentName === 'ReadPrettyFormItem') {
const blockSchema = createDetailsBlockSchema({
actionInitializers,
const blockSchema = createDetailsUISchema(
association
? {
association,
collection: item.collectionName || item.name,
dataSource: item.dataSource,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
template: templateSchema,
settings: 'blockSettings:singleDataDetails',
});
templateSchema: templateSchema,
}
: {
collectionName: item.collectionName || item.name,
dataSource: item.dataSource,
templateSchema: templateSchema,
},
);
if (item.mode === 'reference') {
blockSchema['x-template-key'] = item.template.key;
}
@ -55,7 +54,7 @@ export function useCreateSingleDetailsSchema() {
return templateSchema;
}
},
[actionInitializers, association],
[association],
);
const createSingleDetailsSchema = useCallback(
@ -65,20 +64,21 @@ export function useCreateSingleDetailsSchema() {
insert(templateWrap(template, { item }));
} else {
insert(
createDetailsBlockSchema({
actionInitializers,
createDetailsUISchema(
association
? {
association,
collection: item.collectionName || item.name,
dataSource: item.dataSource,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
settings: 'blockSettings:singleDataDetails',
}),
}
: {
collectionName: item.collectionName || item.name,
dataSource: item.dataSource,
},
),
);
}
},
[actionInitializers, association, getTemplateSchemaByMode, insert, templateWrap],
[association, getTemplateSchemaByMode, insert, templateWrap],
);
return { createSingleDetailsSchema, templateWrap };

View File

@ -0,0 +1,62 @@
import { createDetailsUISchema } from '../createDetailsUISchema';
// Mock uid to always return a fixed value
vi.mock('@formily/shared', () => ({
uid: () => 'fixed-uid',
}));
describe('createDetailsBlockWithoutPagingUISchema', () => {
it('should create a valid schema with mandatory fields', () => {
const options = {
collectionName: 'users',
dataSource: 'usersDataSource',
};
const result = createDetailsUISchema(options);
expect(result).toMatchInlineSnapshot(`
{
"properties": {
"fixed-uid": {
"properties": {
"fixed-uid": {
"properties": {},
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": 24,
},
},
"x-initializer": "details:configureActions",
},
"grid": {
"properties": {},
"type": "void",
"x-component": "Grid",
"x-initializer": "details:configureFields",
},
},
"type": "void",
"x-component": "Details",
"x-read-pretty": true,
"x-use-component-props": "useDetailsProps",
},
},
"type": "void",
"x-acl-action": "users:get",
"x-component": "CardItem",
"x-decorator": "DetailsBlockProvider",
"x-decorator-props": {
"action": "get",
"association": undefined,
"collection": "users",
"dataSource": "usersDataSource",
"readPretty": true,
},
"x-settings": "blockSettings:details",
"x-toolbar": "BlockSchemaToolbar",
"x-use-decorator-props": "useDetailsDecoratorProps",
}
`);
});
});

View File

@ -0,0 +1,60 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export function createDetailsUISchema(options: {
dataSource: string;
collectionName?: string;
association?: string;
templateSchema?: ISchema;
}): ISchema {
const { collectionName, dataSource, association, templateSchema } = options;
const resourceName = association || collectionName;
if (!dataSource) {
throw new Error('dataSource are required');
}
return {
type: 'void',
'x-acl-action': `${resourceName}:get`,
'x-decorator': 'DetailsBlockProvider',
'x-use-decorator-props': 'useDetailsDecoratorProps',
'x-decorator-props': {
dataSource,
collection: collectionName,
association,
readPretty: true,
action: 'get',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:details',
'x-component': 'CardItem',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Details',
'x-read-pretty': true,
'x-use-component-props': 'useDetailsProps',
properties: {
[uid()]: {
type: 'void',
'x-initializer': 'details:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 24,
},
},
properties: {},
},
grid: templateSchema || {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
properties: {},
},
},
},
},
};
}

View File

@ -0,0 +1,56 @@
import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../../collection-manager';
import { SchemaSettingsBlockTitleItem, SchemaSettingsFormItemTemplate } from '../../../../schema-settings';
import { SchemaSettingsItemType } from '../../../../application/schema-settings/types';
const commonItems: SchemaSettingsItemType[] = [
{
name: 'title',
Component: SchemaSettingsBlockTitleItem,
},
{
name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return {
insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem',
collectionName: name,
resourceName: defaultResource,
};
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'remove',
type: 'remove',
componentProps: {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
},
},
];
/**
* @deprecated
* 使 detailsBlockSettings
*/
export const singleDataDetailsBlockSettings = new SchemaSettings({
name: 'blockSettings:singleDataDetails',
items: commonItems,
});
export const detailsBlockSettings = new SchemaSettings({
name: 'blockSettings:details',
items: commonItems,
});

View File

@ -0,0 +1,18 @@
import { useParamsFromRecord } from '../../../../../block-provider/BlockProvider';
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
export function useDetailsDecoratorProps(props) {
const params = useParamsFromRecord();
let sourceId;
// association 的值是固定不变的,所以可以在条件中使用 hooks
if (props.association) {
// eslint-disable-next-line react-hooks/rules-of-hooks
sourceId = useDataBlockSourceId({ association: props.association });
}
return {
params,
sourceId,
};
}

View File

@ -0,0 +1,5 @@
import { useDetailsBlockProps } from '../../../../../block-provider/DetailsBlockProvider';
export function useDetailsProps() {
return useDetailsBlockProps();
}

View File

@ -1,44 +0,0 @@
import { useFieldSchema } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { useCollection_deprecated } from '../../../../collection-manager';
import { SchemaSettingsBlockTitleItem, SchemaSettingsFormItemTemplate } from '../../../../schema-settings';
export const singleDataDetailsBlockSettings = new SchemaSettings({
name: 'blockSettings:singleDataDetails',
items: [
{
name: 'title',
Component: SchemaSettingsBlockTitleItem,
},
{
name: 'formItemTemplate',
Component: SchemaSettingsFormItemTemplate,
useComponentProps() {
const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return {
insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem',
collectionName: name,
resourceName: defaultResource,
};
},
},
{
name: 'divider',
type: 'divider',
},
{
name: 'remove',
type: 'remove',
componentProps: {
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-component': 'Grid',
},
},
},
],
});

View File

@ -9,7 +9,7 @@ import {
oneTableBlockWithAddNewAndViewAndEditAndBasicFields,
test,
} from '@nocobase/test/e2e';
import { T2165, T2174, T3251 } from './templatesOfBug';
import { T2165, T2174, T3251, T3806 } from './templatesOfBug';
const clickOption = async (page: Page, optionName: string) => {
await page.getByLabel('block-item-CardItem-general-form').hover();
@ -254,6 +254,47 @@ test.describe('creation form block schema settings', () => {
page.getByLabel('block-item-CollectionField-users-form-users.email-Email').getByRole('textbox'),
).toBeEditable();
});
// https://nocobase.height.app/T-3806
test('after save as block template', async ({ page, mockPage }) => {
await mockPage(T3806).goto();
// 1. 一开始联动规则应该正常
await page
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
.getByRole('textbox')
.fill('123');
await expect(
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
).toHaveValue('123');
try {
// 2. 将表单区块保存为模板后
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: 'Save as block template' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await page.waitForTimeout(1000);
// 3. 联动规则应该依然是正常的
await page
.getByLabel('block-item-CollectionField-users-form-users.nickname-Nickname')
.getByRole('textbox')
.fill('456');
await expect(
page.getByLabel('block-item-CollectionField-users-form-users.username-Username').getByRole('textbox'),
).toHaveValue('456');
} catch (err) {
throw err;
} finally {
// 4. 把创建的模板删除
await page.goto('/admin/settings/ui-schema-storage');
await page.getByLabel('Select all').check();
await page.getByLabel('action-Action-Delete-destroy-').click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
await expect(page.getByRole('row', { name: 'Users_Form' }).first()).toBeHidden();
}
});
});
test('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {

View File

@ -6066,3 +6066,204 @@ export const T3529: PageConfig = {
'x-index': 1,
},
};
export const T3806: PageConfig = {
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
'6w8tkq3665g': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
'0k0yifwylj8': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
v5cdqnvyh3z: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
p9pszqbz1n5: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 'users:create',
'x-decorator': 'FormBlockProvider',
'x-use-decorator-props': 'useCreateFormBlockDecoratorProps',
'x-decorator-props': {
dataSource: 'main',
collection: 'users',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:createForm',
'x-component': 'CardItem',
properties: {
'578cg3ao40s': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useCreateFormBlockProps',
properties: {
grid: {
'x-uid': 'xpdqjz634mt',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'form:configureFields',
'x-linkage-rules': [
{
condition: {
$and: [],
},
actions: [
{
targetFields: ['username'],
operator: 'value',
value: {
mode: 'express',
value: '{{$nForm.nickname}}',
result: '{{$nForm.nickname}}',
},
},
],
},
],
properties: {
gjck905cyws: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
i041myizzph: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
nickname: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.nickname',
'x-component-props': {},
'x-uid': 'afnfoxl5cbi',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'a3o7l08zz6e',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'zoakp82dymw',
'x-async': false,
'x-index': 1,
},
qffoo7lqua1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
nq0rbuti1d9: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
username: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-toolbar': 'FormItemSchemaToolbar',
'x-settings': 'fieldSettings:FormItem',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field': 'users.username',
'x-component-props': {},
'x-uid': 'mq80n8nyuq8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'hf9bq4vjm80',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ua08izsl514',
'x-async': false,
'x-index': 2,
},
},
'x-async': false,
'x-index': 1,
},
'006ip4mnr75': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'createForm:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-uid': 'u6d026lia1x',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'p7ucl8ixcx4',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'hz9xoui3o9d',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'j1q201o2wju',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 't0olm6ygvas',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'aq4i1y0nf00',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'px8uapyslvw',
'x-async': true,
'x-index': 1,
},
};

View File

@ -2,7 +2,7 @@ import { OrderedListOutlined } from '@ant-design/icons';
import React from 'react';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { useCollectionManager_deprecated } from '../../../../collection-manager';
import { createGridCardBlockSchema } from '../../../../schema-initializer/utils';
import { createGridCardBlockSchema } from './createGridCardBlockSchema';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection';
@ -48,10 +48,9 @@ export const GridCardBlockInitializer = ({
const collection = getCollection(item.name, item.dataSource);
const schema = createGridCardBlockSchema({
collection: item.name,
collectionName: item.name,
dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:gridCard',
});
insert(schema);
}}

View File

@ -0,0 +1,158 @@
import { createGridCardBlockSchema } from '../createGridCardBlockSchema';
describe('createGridCardBlockSchema', () => {
test('should return the correct schema', () => {
const options = {
collectionName: 'testCollection',
dataSource: 'testDataSource',
association: 'testAssociation',
templateSchema: { type: 'string' },
rowKey: 'testRowKey',
};
const schema = createGridCardBlockSchema(options);
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"actionBar": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": "var(--nb-spacing)",
},
},
"x-initializer": "gridCard:configureActions",
},
"list": {
"properties": {
"item": {
"properties": {
"actionBar": {
"type": "void",
"x-align": "left",
"x-component": "ActionBar",
"x-component-props": {
"layout": "one-column",
},
"x-initializer": "gridCard:configureItemActions",
"x-use-component-props": "useGridCardActionBarProps",
},
"grid": {
"type": "string",
},
},
"type": "object",
"x-component": "GridCard.Item",
"x-read-pretty": true,
"x-use-component-props": "useGridCardItemProps",
},
},
"type": "array",
"x-component": "GridCard",
"x-use-component-props": "useGridCardBlockProps",
},
},
"type": "void",
"x-acl-action": "testAssociation:view",
"x-component": "BlockItem",
"x-decorator": "GridCard.Decorator",
"x-decorator-props": {
"action": "list",
"association": "testAssociation",
"collection": "testCollection",
"dataSource": "testDataSource",
"params": {
"pageSize": 12,
},
"readPretty": true,
"rowKey": "testRowKey",
"runWhenParamsChanged": true,
},
"x-settings": "blockSettings:gridCard",
"x-toolbar": "BlockSchemaToolbar",
"x-use-component-props": "useGridCardBlockItemProps",
"x-use-decorator-props": "useGridCardBlockDecoratorProps",
}
`);
});
test('should return the correct schema when templateSchema is empty', () => {
const options = {
collectionName: 'testCollection',
dataSource: 'testDataSource',
association: 'testAssociation',
rowKey: 'testRowKey',
};
const schema = createGridCardBlockSchema(options);
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"actionBar": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": "var(--nb-spacing)",
},
},
"x-initializer": "gridCard:configureActions",
},
"list": {
"properties": {
"item": {
"properties": {
"actionBar": {
"type": "void",
"x-align": "left",
"x-component": "ActionBar",
"x-component-props": {
"layout": "one-column",
},
"x-initializer": "gridCard:configureItemActions",
"x-use-component-props": "useGridCardActionBarProps",
},
"grid": {
"type": "void",
"x-component": "Grid",
"x-initializer": "details:configureFields",
},
},
"type": "object",
"x-component": "GridCard.Item",
"x-read-pretty": true,
"x-use-component-props": "useGridCardItemProps",
},
},
"type": "array",
"x-component": "GridCard",
"x-use-component-props": "useGridCardBlockProps",
},
},
"type": "void",
"x-acl-action": "testAssociation:view",
"x-component": "BlockItem",
"x-decorator": "GridCard.Decorator",
"x-decorator-props": {
"action": "list",
"association": "testAssociation",
"collection": "testCollection",
"dataSource": "testDataSource",
"params": {
"pageSize": 12,
},
"readPretty": true,
"rowKey": "testRowKey",
"runWhenParamsChanged": true,
},
"x-settings": "blockSettings:gridCard",
"x-toolbar": "BlockSchemaToolbar",
"x-use-component-props": "useGridCardBlockItemProps",
"x-use-decorator-props": "useGridCardBlockDecoratorProps",
}
`);
});
});

View File

@ -0,0 +1,81 @@
import { ISchema } from '@formily/react';
export const createGridCardBlockSchema = (options: {
dataSource: string;
collectionName?: string;
association?: string;
templateSchema?: ISchema;
rowKey?: string;
}): ISchema => {
const { collectionName, association, templateSchema, dataSource, rowKey } = options;
const resourceName = association || collectionName;
if (!dataSource) {
throw new Error('dataSource are required');
}
return {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-use-decorator-props': 'useGridCardBlockDecoratorProps',
'x-decorator-props': {
collection: collectionName,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
rowKey,
},
'x-component': 'BlockItem',
'x-use-component-props': 'useGridCardBlockItemProps',
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:gridCard',
properties: {
actionBar: {
type: 'void',
'x-initializer': 'gridCard:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
},
list: {
type: 'array',
'x-component': 'GridCard',
'x-use-component-props': 'useGridCardBlockProps',
properties: {
item: {
type: 'object',
'x-component': 'GridCard.Item',
'x-read-pretty': true,
'x-use-component-props': 'useGridCardItemProps',
properties: {
grid: templateSchema || {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'details:configureFields',
},
actionBar: {
type: 'void',
'x-align': 'left',
'x-initializer': 'gridCard:configureItemActions',
'x-component': 'ActionBar',
'x-use-component-props': 'useGridCardActionBarProps',
'x-component-props': {
layout: 'one-column',
},
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,18 @@
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
import { useGridCardBlockParams } from './useGridCardBlockParams';
export function useGridCardBlockDecoratorProps(props) {
const params = useGridCardBlockParams(props);
let sourceId;
// 因为 association 是固定的,所以可以在条件中使用 hooks
if (props.association) {
// eslint-disable-next-line react-hooks/rules-of-hooks
sourceId = useDataBlockSourceId({ association: props.association });
}
return {
params,
sourceId,
};
}

View File

@ -0,0 +1,17 @@
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
import { useMemo } from 'react';
export function useGridCardBlockParams(props) {
const { params } = props;
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return paramsWithFilter;
}

View File

@ -1,7 +1,7 @@
import { FormOutlined } from '@ant-design/icons';
import React from 'react';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application';
import { createFilterFormBlockSchema } from '../../../../schema-initializer/utils';
import { createFilterFormBlockSchema } from './createFilterFormBlockSchema';
import { FilterBlockInitializer } from '../../../../schema-initializer/items/FilterBlockInitializer';
import { Collection, CollectionFieldOptions } from '../../../../data-source';
@ -25,10 +25,9 @@ export const FilterFormBlockInitializer = ({
componentType={'FilterFormItem'}
templateWrap={(templateSchema, { item }) => {
const s = createFilterFormBlockSchema({
template: templateSchema,
templateSchema: templateSchema,
dataSource: item.dataSource,
collection: item.name || item.collectionName,
settings: 'blockSettings:filterForm',
collectionName: item.name || item.collectionName,
});
if (item.template && item.mode === 'reference') {
s['x-template-key'] = item.template.key;
@ -38,9 +37,8 @@ export const FilterFormBlockInitializer = ({
onCreateBlockSchema={({ item }) => {
return insert(
createFilterFormBlockSchema({
collection: item.collectionName || item.name,
collectionName: item.collectionName || item.name,
dataSource: item.dataSource,
settings: 'blockSettings:filterForm',
}),
);
}}

View File

@ -0,0 +1,106 @@
import { createFilterFormBlockSchema } from '../createFilterFormBlockSchema';
vi.mock('@formily/shared', () => {
return {
uid: () => 'mocked-uid',
};
});
describe('createFilterFormBlockSchema', () => {
test('should return the correct schema', () => {
const schema = createFilterFormBlockSchema({
collectionName: 'myCollection',
dataSource: 'myDataSource',
templateSchema: { type: 'string' },
});
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"mocked-uid": {
"properties": {
"grid": {
"type": "string",
},
"mocked-uid": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"layout": "one-column",
"style": {
"float": "right",
},
},
"x-initializer": "filterForm:configureActions",
},
},
"type": "void",
"x-component": "FormV2",
"x-use-component-props": "useFilterFormBlockProps",
},
},
"type": "void",
"x-component": "CardItem",
"x-decorator": "FilterFormBlockProvider",
"x-decorator-props": {
"collection": "myCollection",
"dataSource": "myDataSource",
},
"x-filter-operators": {},
"x-filter-targets": [],
"x-settings": "blockSettings:filterForm",
"x-toolbar": "BlockSchemaToolbar",
"x-use-decorator-props": "useFilterFormBlockDecoratorProps",
}
`);
});
test('should return the correct schema without templateSchema', () => {
const schema = createFilterFormBlockSchema({
collectionName: 'myCollection',
dataSource: 'myDataSource',
});
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"mocked-uid": {
"properties": {
"grid": {
"type": "void",
"x-component": "Grid",
"x-initializer": "filterForm:configureFields",
},
"mocked-uid": {
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"layout": "one-column",
"style": {
"float": "right",
},
},
"x-initializer": "filterForm:configureActions",
},
},
"type": "void",
"x-component": "FormV2",
"x-use-component-props": "useFilterFormBlockProps",
},
},
"type": "void",
"x-component": "CardItem",
"x-decorator": "FilterFormBlockProvider",
"x-decorator-props": {
"collection": "myCollection",
"dataSource": "myDataSource",
},
"x-filter-operators": {},
"x-filter-targets": [],
"x-settings": "blockSettings:filterForm",
"x-toolbar": "BlockSchemaToolbar",
"x-use-decorator-props": "useFilterFormBlockDecoratorProps",
}
`);
});
});

View File

@ -0,0 +1,57 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export const createFilterFormBlockSchema = (options: {
collectionName: string;
dataSource: string;
templateSchema?: ISchema;
}) => {
const { collectionName, dataSource, templateSchema } = options;
if (!collectionName || !dataSource) {
throw new Error('collectionName and dataSource are required');
}
const schema: ISchema = {
type: 'void',
'x-decorator': 'FilterFormBlockProvider',
'x-use-decorator-props': 'useFilterFormBlockDecoratorProps',
'x-decorator-props': {
dataSource,
collection: collectionName,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:filterForm',
'x-component': 'CardItem',
// 保存当前筛选区块所能过滤的数据区块
'x-filter-targets': [],
// 用于存储用户设置的每个字段的运算符,目前仅筛选表单区块支持自定义
'x-filter-operators': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-use-component-props': 'useFilterFormBlockProps',
properties: {
grid: templateSchema || {
type: 'void',
'x-component': 'Grid',
'x-initializer': 'filterForm:configureFields',
},
[uid()]: {
type: 'void',
'x-initializer': 'filterForm:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
float: 'right',
},
},
},
},
},
},
};
return schema;
};

View File

@ -0,0 +1 @@
export function useFilterFormBlockDecoratorProps() {}

View File

@ -0,0 +1,5 @@
import { useFormBlockProps } from '../../../../../block-provider/FormBlockProvider';
export function useFilterFormBlockProps() {
return useFormBlockProps();
}

View File

@ -88,7 +88,7 @@ test.describe('where to open a popup and what can be added to it', () => {
// 删除已创建的 blocks腾出页面空间
// delete details block
await page.getByText('GeneralConfigure actionsConfigure fields').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:singleDataDetails-general').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:details-general').hover();
await page.getByRole('menuitem', { name: 'Delete' }).click();
await page.getByRole('button', { name: 'OK', exact: true }).click();
// delete form block

View File

@ -6,7 +6,7 @@ import { useFieldComponentName } from '../../../../common/useFieldComponentName'
import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component';
import { isSubMode } from '../../../../schema-component/antd/association-field/util';
import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
const fieldComponent: any = {
name: 'fieldComponent',

View File

@ -9,7 +9,7 @@ import {
useIsFieldReadPretty,
useIsFormReadPretty,
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
import { useFormBlockType } from '../../../../block-provider';
const allowMultiple: any = {

View File

@ -10,7 +10,7 @@ import {
useTitleFieldOptions,
} from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
export const titleField: any = {
name: 'titleField',

View File

@ -6,7 +6,7 @@ import { useFieldComponentName } from '../../../../common/useFieldComponentName'
import { useDesignable, useFieldModeOptions, useIsAddNewForm } from '../../../../schema-component';
import { isSubMode } from '../../../../schema-component/antd/association-field/util';
import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { titleField } from '../Picker/recordPickerComponentFieldSettings';

View File

@ -23,7 +23,7 @@ import { SchemaSettingsDataScope } from '../../../../schema-settings/SchemaSetti
import { SchemaSettingsSortingRule } from '../../../../schema-settings/SchemaSettingsSortingRule';
import { useIsShowMultipleSwitch } from '../../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { useLocalVariables, useVariables } from '../../../../variables';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
const enableLink = {
name: 'enableLink',

View File

@ -8,7 +8,7 @@ import { isSubMode } from '../../../../schema-component/antd/association-field/u
import { useTitleFieldOptions } from '../../../../schema-component/antd/form-item/FormItem.Settings';
import { useColorFields } from '../../../../schema-component/antd/table-v2/Table.Column.Designer';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { useCollectionField } from '../utils';
import { useCollectionField } from '../../../../data-source';
const enableLink = {
name: 'enableLink',
@ -140,9 +140,12 @@ export const tagComponentFieldSettings = new SchemaSettings({
useComponentProps() {
const { t } = useTranslation();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const schema = useFieldSchema();
const targetCollectionField = useCollectionField();
const { fieldSchema: tableColumnSchema, collectionField: tableColumnField } = useColumnSchema();
const fieldSchema = tableColumnSchema || schema;
const collectionField = tableColumnField || targetCollectionField;
const { dn } = useDesignable();
const collectionField = useCollectionField();
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
return {
title: t('Tag color field'),

View File

@ -1,14 +0,0 @@
import { useFieldSchema } from '@formily/react';
import { useCollection_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
/**
*
* @returns
*/
export function useCollectionField() {
const { getCollectionJoinField } = useCollectionManager_deprecated();
const { getField } = useCollection_deprecated();
const fieldSchema = useFieldSchema();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
return collectionField;
}

View File

@ -33,7 +33,7 @@ export const PoweredBy = () => {
dangerouslySetInnerHTML={{
__html: parseHTML(
customBrandPlugin?.options?.options?.brand ||
`Powered by <a href="${urls[i18n.language] || urls['en-US']}">NocoBase</a>`,
`Powered by <a href="${urls[i18n.language] || urls['en-US']}" target="__blank">NocoBase</a>`,
{ appVersion },
),
}}

View File

@ -5,9 +5,12 @@ import { SortableItem } from '../../common';
import { useDesigner, useProps } from '../../hooks';
import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem';
import { useFieldSchema } from '@formily/react';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
export const BlockItem: React.FC<any> = (props) => {
export const BlockItem: React.FC<any> = withDynamicSchemaProps((props) => {
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { className, children } = useProps(props);
const Designer = useDesigner();
const fieldSchema = useFieldSchema();
const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name);
@ -69,4 +72,4 @@ export const BlockItem: React.FC<any> = (props) => {
{children}
</SortableItem>
);
};
});

View File

@ -3,8 +3,9 @@ import { FormV2 } from '../form-v2';
import _ from 'lodash';
import { Empty } from 'antd';
import { useDataBlockRequest } from '../../../data-source';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
export const Details = (props) => {
export const Details = withDynamicSchemaProps((props) => {
const request = useDataBlockRequest();
if (!request?.loading && _.isEmpty(request?.data?.data)) {
@ -12,4 +13,4 @@ export const Details = (props) => {
}
return <FormV2 {...props} />;
};
});

View File

@ -67,16 +67,15 @@ export const FormItem: any = observer(
},
);
}, [showTitle]);
const fieldSchema = useFieldSchema();
return (
<CollectionFieldProvider allowNull={true}>
<BlockItem className={'nb-form-item'}>
<CollectionFieldProvider name={fieldSchema.name} allowNull={!fieldSchema['x-collection-field']}>
<ACLCollectionFieldProvider>
<Item className={className} {...props} extra={extra} />
</ACLCollectionFieldProvider>
</CollectionFieldProvider>
</BlockItem>
</CollectionFieldProvider>
);
},
{ displayName: 'FormItem' },

View File

@ -19,6 +19,7 @@ import { getInnermostKeyAndValue, getTargetField } from '../../common/utils/uitl
import { useProps } from '../../hooks/useProps';
import { collectFieldStateOfLinkageRules, getTempFieldState } from './utils';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useTemplateBlockContext } from '../../../block-provider/TemplateBlockProvider';
export interface FormProps {
[key: string]: any;
@ -87,6 +88,7 @@ const WithForm = (props: WithFormProps) => {
const { setFormValueChanged } = useActionContext();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const { templateFinshed } = useTemplateBlockContext();
const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
@ -151,7 +153,7 @@ const WithForm = (props: WithFormProps) => {
return result;
},
getSubscriber(action, field, rule, variables, localVariables),
{ fireImmediately: true },
{ fireImmediately: false },
),
);
});
@ -166,7 +168,7 @@ const WithForm = (props: WithFormProps) => {
dispose();
});
};
}, [linkageRules]);
}, [linkageRules, templateFinshed]);
return fieldSchema['x-decorator'] === 'FormV2' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
};

View File

@ -101,6 +101,7 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer(
async onSuccess(data) {
await form.reset();
form.setValues(data?.data);
form.setInitialValues(data?.data);
},
},
props,

View File

@ -1,9 +1,12 @@
import { FormLayout } from '@formily/antd-v5';
import { createForm } from '@formily/core';
import { FormContext, useField } from '@formily/react';
import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { BlockProvider, useBlockRequestContext, useParsedFilter } from '../../../block-provider';
import useStyles from './GridCard.Decorator.style';
import { useGridCardBlockParams } from '../../../modules/blocks/data-blocks/grid-card/hooks/useGridCardBlockParams';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
export const GridCardBlockContext = createContext<any>({});
GridCardBlockContext.displayName = 'GridCardBlockContext';
@ -41,24 +44,31 @@ const InternalGridCardBlockProvider = (props) => {
);
};
export const GridCardBlockProvider = (props) => {
const { params } = props;
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
/**
* UISchema x-use-decorator-props
* @param props
*/
const useCompatGridCardBlockParams = (props) => {
const schema = useFieldSchema();
// 因为 x-use-decorator-props 的值是固定的,所以可以在条件中使用 hooks
if (schema['x-use-decorator-props']) {
return props.params;
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useGridCardBlockParams(props);
}
};
}, [parsedFilter, params]);
export const GridCardBlockProvider = withDynamicSchemaProps((props) => {
const params = useCompatGridCardBlockParams(props);
return (
<BlockProvider name="grid-card" {...props} params={paramsWithFilter}>
<BlockProvider name="grid-card" {...props} params={params}>
<InternalGridCardBlockProvider {...props} />
</BlockProvider>
);
};
});
export const useGridCardBlockContext = () => {
return useContext(GridCardBlockContext);

View File

@ -5,6 +5,7 @@ import { Card } from 'antd';
import React from 'react';
import { useCollectionParentRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
import { RecordProvider } from '../../../record-provider';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
const itemCss = css`
display: flex;
@ -15,7 +16,7 @@ const itemCss = css`
gap: 8px;
`;
export const GridCardItem = (props) => {
export const GridCardItem = withDynamicSchemaProps((props) => {
const field = useField<ObjectField>();
const parentRecordData = useCollectionParentRecordData();
return (
@ -40,4 +41,4 @@ export const GridCardItem = (props) => {
</div>
</Card>
);
};
});

View File

@ -11,6 +11,7 @@ import { GridCardDesigner } from './GridCard.Designer';
import { GridCardItem } from './GridCard.Item';
import { useGridCardActionBarProps } from './hooks';
import { defaultColumnCount, pageSizeOptions } from './options';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
const rowGutter = {
md: 12,
@ -56,7 +57,9 @@ const designerCss = css`
`;
const InternalGridCard = (props) => {
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { columnCount: columnCountProp, pagination } = useProps(props);
const { service, columnCount: _columnCount = defaultColumnCount } = useGridCardBlockContext();
const columnCount = columnCountProp || _columnCount;
const { run, params } = service;
@ -145,7 +148,7 @@ const InternalGridCard = (props) => {
);
};
export const GridCard = InternalGridCard as typeof InternalGridCard & {
export const GridCard = withDynamicSchemaProps(InternalGridCard) as typeof InternalGridCard & {
Item: typeof GridCardItem;
Designer: typeof GridCardDesigner;
Decorator: typeof GridCardBlockProvider;

View File

@ -1,15 +0,0 @@
import { render } from '@nocobase/test/client';
import React from 'react';
import { Pagination } from '../index';
describe('Pagination', () => {
it('should render correctly', () => {
render(<Pagination />);
expect(document.querySelector('.ant-pagination')).toBeInTheDocument();
});
it('hidden', () => {
render(<Pagination hidden />);
expect(document.querySelector('.ant-pagination')).not.toBeInTheDocument();
});
});

View File

@ -2,10 +2,14 @@ import { observer } from '@formily/react';
import { Pagination as AntdPagination } from 'antd';
import React, { KeyboardEventHandler } from 'react';
import { useProps } from '../../hooks/useProps';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
export const Pagination = observer(
export const Pagination = withDynamicSchemaProps(
observer(
(props: any) => {
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { hidden, ...others } = useProps(props);
if (hidden) {
return null;
}
@ -22,4 +26,5 @@ export const Pagination = observer(
);
},
{ displayName: 'Pagination' },
),
);

View File

@ -52,7 +52,8 @@ const InternalRemoteSelect = connect(
const { getField } = useCollection_deprecated();
const searchData = useRef(null);
const { getCollectionJoinField, getInterface } = useCollectionManager_deprecated();
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema.name as string);
const colletionFieldName = fieldSchema['x-collection-field'] || fieldSchema.name;
const collectionField = getField(colletionFieldName) || getCollectionJoinField(colletionFieldName);
const targetField =
_targetField ||
(collectionField?.target &&

View File

@ -123,7 +123,6 @@ export * from './buttons';
export * from './items';
export {
createDetailsBlockSchema,
createFilterFormBlockSchema,
createFormBlockSchema,
createReadPrettyFormBlockSchema,
createTableBlockSchema,

View File

@ -4,7 +4,8 @@ import React, { useCallback } from 'react';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
import { useCollectionManager_deprecated } from '../../collection-manager';
import { useSchemaTemplateManager } from '../../schema-templates';
import { createDetailsBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
import { useRecordCollectionDataSourceItems } from '../utils';
import { createDetailsWithPaginationUISchema } from '../../modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema';
export const RecordAssociationDetailsBlockInitializer = () => {
const itemConfig = useSchemaInitializerItem();
@ -25,13 +26,10 @@ export const RecordAssociationDetailsBlockInitializer = () => {
insert(s);
} else {
insert(
createDetailsBlockSchema({
collection: field.target,
resource,
createDetailsWithPaginationUISchema({
dataSource: collection.dataSource,
association: resource,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:multiDataDetails',
}),
);
}
@ -51,12 +49,10 @@ export function useCreateAssociationDetailsBlock() {
const collection = getCollection(field.target);
insert(
createDetailsBlockSchema({
collection: field.target,
createDetailsWithPaginationUISchema({
dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`,
rowKey: collection.filterTargetKey || 'id',
settings: 'blockSettings:multiDataDetails',
}),
);
},

View File

@ -4,7 +4,8 @@ import React, { useCallback } from 'react';
import { useCollectionManager_deprecated } from '../../collection-manager';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
import { useSchemaTemplateManager } from '../../schema-templates';
import { createGridCardBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
import { useRecordCollectionDataSourceItems } from '../utils';
import { createGridCardBlockSchema } from '../../modules/blocks/data-blocks/grid-card/createGridCardBlockSchema';
/**
* @deprecated
@ -31,11 +32,8 @@ export const RecordAssociationGridCardBlockInitializer = () => {
insert(
createGridCardBlockSchema({
rowKey: collection.filterTargetKey,
collection: field.target,
resource,
dataSource: collection.dataSource,
association: resource,
settings: 'blockSettings:gridCard',
}),
);
}
@ -57,10 +55,8 @@ export function useCreateAssociationGridCardBlock() {
insert(
createGridCardBlockSchema({
rowKey: collection.filterTargetKey,
collection: field.target,
dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`,
settings: 'blockSettings:gridCard',
}),
);
},

View File

@ -2,10 +2,10 @@ import { FormOutlined } from '@ant-design/icons';
import React, { useCallback } from 'react';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
import { useBlockRequestContext } from '../../block-provider';
import { useSchemaTemplateManager } from '../../schema-templates';
import { createDetailsBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
import { useRecordCollectionDataSourceItems } from '../utils';
import { useCollectionManager_deprecated } from '../../collection-manager';
import { createDetailsUISchema } from '../../modules/blocks/data-blocks/details-single/createDetailsUISchema';
/**
* @deprecated
@ -22,8 +22,6 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => {
const collection = getCollection(collectionName);
const resource = `${field.collectionName}.${field.name}`;
const { block } = useBlockRequestContext();
const actionInitializers = block !== 'TableField' ? 'details:configureActions' : null;
return (
<SchemaInitializerItem
@ -33,17 +31,10 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => {
if (item.template) {
const s = await getTemplateSchemaByMode(item);
if (item.template.componentName === 'ReadPrettyFormItem') {
const blockSchema = createDetailsBlockSchema({
actionInitializers,
collection: collectionName,
const blockSchema = createDetailsUISchema({
dataSource: collection.dataSource,
resource,
association: resource,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
template: s,
settings: 'blockSettings:singleDataDetails',
templateSchema: s,
});
if (item.mode === 'reference') {
blockSchema['x-template-key'] = item.template.key;
@ -54,16 +45,9 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => {
}
} else {
insert(
createDetailsBlockSchema({
actionInitializers,
collection: collectionName,
resource,
createDetailsUISchema({
association: resource,
dataSource: collection.dataSource,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
settings: 'blockSettings:singleDataDetails',
}),
);
}
@ -76,8 +60,6 @@ export const RecordReadPrettyAssociationFormBlockInitializer = () => {
export function useCreateAssociationDetailsWithoutPagination() {
const { insert } = useSchemaInitializer();
const { getCollection } = useCollectionManager_deprecated();
const { block } = useBlockRequestContext();
const actionInitializers = block !== 'TableField' ? 'details:configureActions' : null;
const createAssociationDetailsWithoutPagination = useCallback(
({ item }) => {
@ -85,19 +67,13 @@ export function useCreateAssociationDetailsWithoutPagination() {
const collection = getCollection(field.target);
insert(
createDetailsBlockSchema({
actionInitializers,
collection: field.target,
createDetailsUISchema({
dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
settings: 'blockSettings:singleDataDetails',
}),
);
},
[actionInitializers, getCollection, insert],
[getCollection, insert],
);
const templateWrap = useCallback(
@ -106,16 +82,10 @@ export function useCreateAssociationDetailsWithoutPagination() {
const collection = getCollection(field.target);
if (item.template.componentName === 'ReadPrettyFormItem') {
const blockSchema = createDetailsBlockSchema({
actionInitializers,
collection: field.target,
const blockSchema = createDetailsUISchema({
dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`,
action: 'get',
useSourceId: '{{ useSourceIdFromParentRecord }}',
useParams: '{{ useParamsFromRecord }}',
template: templateSchema,
settings: 'blockSettings:singleDataDetails',
templateSchema: templateSchema,
});
if (item.mode === 'reference') {
blockSchema['x-template-key'] = item.template.key;
@ -125,7 +95,7 @@ export function useCreateAssociationDetailsWithoutPagination() {
return templateSchema;
}
},
[actionInitializers, getCollection],
[getCollection],
);
return { createAssociationDetailsWithoutPagination, templateWrap };

View File

@ -893,7 +893,18 @@ export const useCollectionDataSourceItems = ({
return res;
};
export const createDetailsBlockSchema = (options) => {
export const createDetailsBlockSchema = (options: {
collection: string;
dataSource: string;
rowKey?: string;
formItemInitializers?: string;
actionInitializers?: string;
association?: string;
template?: any;
settings?: string;
action?: string;
[key: string]: any;
}) => {
const {
formItemInitializers = 'details:configureFields',
actionInitializers = 'detailsWithPaging:configureActions',
@ -972,97 +983,6 @@ export const createDetailsBlockSchema = (options) => {
return schema;
};
export const createGridCardBlockSchema = (options) => {
const {
formItemInitializers = 'details:configureFields',
actionInitializers = 'gridCard:configureActions',
itemActionInitializers = 'gridCard:configureItemActions',
collection,
association,
template,
dataSource,
settings,
...others
} = options;
const resourceName = association || collection;
const schema: ISchema = {
type: 'void',
'x-acl-action': `${resourceName}:view`,
'x-decorator': 'GridCard.Decorator',
'x-decorator-props': {
collection,
association,
dataSource,
readPretty: true,
action: 'list',
params: {
pageSize: 12,
},
runWhenParamsChanged: true,
...others,
},
'x-component': 'BlockItem',
'x-component-props': {
useProps: '{{ useGridCardBlockItemProps }}',
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': settings,
properties: {
actionBar: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {},
},
list: {
type: 'array',
'x-component': 'GridCard',
'x-component-props': {
useProps: '{{ useGridCardBlockProps }}',
},
properties: {
item: {
type: 'object',
'x-component': 'GridCard.Item',
'x-read-pretty': true,
'x-component-props': {
useProps: '{{ useGridCardItemProps }}',
},
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
'x-initializer-props': {
useProps: '{{ useGridCardItemInitializerProps }}',
},
properties: {},
},
actionBar: {
type: 'void',
'x-align': 'left',
'x-initializer': itemActionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
useProps: '{{ useGridCardActionBarProps }}',
layout: 'one-column',
},
properties: {},
},
},
},
},
},
},
};
return schema;
};
/**
* @deprecated
* 使 createCreateFormBlockUISchema createEditFormBlockUISchema
@ -1153,71 +1073,6 @@ export const createFormBlockSchema = (options: {
return schema;
};
export const createFilterFormBlockSchema = (options) => {
const {
formItemInitializers = 'filterForm:configureFields',
actionInitializers = 'filterForm:configureActions',
collection,
resource,
association,
dataSource,
action,
template,
settings,
...others
} = options;
const resourceName = resource || association || collection;
const schema: ISchema = {
type: 'void',
'x-decorator': 'FilterFormBlockProvider',
'x-decorator-props': {
...others,
action,
resource: resourceName,
dataSource,
collection,
association,
},
'x-toolbar': 'BlockSchemaToolbar',
...(settings ? { 'x-settings': settings } : { 'x-designer': 'FormV2.FilterDesigner' }),
'x-component': 'CardItem',
// 保存当前筛选区块所能过滤的数据区块
'x-filter-targets': [],
// 用于存储用户设置的每个字段的运算符,目前仅筛选表单区块支持自定义
'x-filter-operators': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: template || {
type: 'void',
'x-component': 'Grid',
'x-initializer': formItemInitializers,
properties: {},
},
[uid()]: {
type: 'void',
'x-initializer': actionInitializers,
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
float: 'right',
},
},
properties: {},
},
},
},
},
};
return schema;
};
/**
* @deprecated
* 使 createDetailsBlockSchema

View File

@ -42,11 +42,11 @@ import {
ActionContextProvider,
AssociationOrCollectionProvider,
CollectionFieldOptions_deprecated,
CollectionRecordProvider,
DataSourceApplicationProvider,
Designable,
FormDialog,
FormProvider,
CollectionRecordProvider,
RemoteSchemaComponent,
SchemaComponent,
SchemaComponentContext,
@ -56,6 +56,7 @@ import {
useAPIClient,
useBlockRequestContext,
useCollectionManager_deprecated,
useCollectionRecord,
useCollection_deprecated,
useCompile,
useDataBlockProps,
@ -63,7 +64,6 @@ import {
useFilterBlock,
useGlobalTheme,
useLinkageCollectionFilterOptions,
useCollectionRecord,
useRecord,
useSchemaSettingsItem,
useSortFields,
@ -84,9 +84,9 @@ import {
useFormActiveFields,
} from '../block-provider/hooks';
import { SelectWithTitle, SelectWithTitleProps } from '../common/SelectWithTitle';
import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight';
import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider';
import { useDataSourceKey } from '../data-source/data-source/DataSourceProvider';
import { useNiceDropdownMaxHeight } from '../common/useNiceDropdownHeight';
import {
FilterBlockType,
getSupportFieldsByAssociation,
@ -574,7 +574,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
field.required = false;
fieldSchema['required'] = false;
}
if (template && ctx?.dn) {
if (template && template.uid === fieldSchema['x-uid'] && ctx?.dn) {
await ctx?.dn.remove(null, options);
} else {
await dn.remove(null, options);

View File

@ -15,8 +15,14 @@ import { customizeUpdateRecordActionSettings } from '../modules/actions/update-r
import { customizePopupActionSettings } from '../modules/actions/view-edit-popup/customizePopupActionSettings';
import { editActionSettings } from '../modules/actions/view-edit-popup/editActionSettings';
import { viewActionSettings } from '../modules/actions/view-edit-popup/viewActionSettings';
import { multiDataDetailsBlockSettings } from '../modules/blocks/data-blocks/details-multi/multiDataDetailsBlockSettings';
import { singleDataDetailsBlockSettings } from '../modules/blocks/data-blocks/details-single/singleDataDetailsBlockSettings';
import {
detailsWithPaginationSettings,
multiDataDetailsBlockSettings,
} from '../modules/blocks/data-blocks/details-multi/detailsWithPaginationSettings';
import {
detailsBlockSettings,
singleDataDetailsBlockSettings,
} from '../modules/blocks/data-blocks/details-single/detailsBlockSettings';
import {
createFormBlockSettings,
creationFormBlockSettings,
@ -57,7 +63,9 @@ export class SchemaSettingsPlugin extends Plugin {
this.schemaSettingsManager.add(filterFormBlockSettings);
this.schemaSettingsManager.add(filterFormItemFieldSettings);
this.schemaSettingsManager.add(multiDataDetailsBlockSettings);
this.schemaSettingsManager.add(detailsWithPaginationSettings);
this.schemaSettingsManager.add(singleDataDetailsBlockSettings);
this.schemaSettingsManager.add(detailsBlockSettings);
this.schemaSettingsManager.add(tableSelectorBlockSettings);
this.schemaSettingsManager.add(listBlockSettings);
this.schemaSettingsManager.add(gridCardBlockSettings);

View File

@ -111,6 +111,8 @@ export const Help = observer(
<Popover
// nb-about 的样式定义在 plugin-custom-brand 插件中
rootClassName="nb-about"
placement="bottomRight"
arrow={false}
content={<div dangerouslySetInnerHTML={{ __html: content }}></div>}
>
{icon}

View File

@ -1,3 +1,3 @@
import { defineConfig } from '@nocobase/test';
import { defineConfig } from '@nocobase/test/vitest.mjs';
export default defineConfig();

View File

@ -29,12 +29,12 @@ const availableActions: {
update: {
displayName: '{{t("Edit")}}',
type: 'old-data',
aliases: ['update', 'move'],
aliases: ['update', 'move', 'add', 'set', 'remove', 'toggle'],
allowConfigureFields: true,
},
destroy: {
displayName: '{{t("Delete")}}',
aliases: ['destroy', 'remove'],
aliases: ['destroy'],
type: 'old-data',
},
};

View File

@ -357,19 +357,25 @@ export class PluginManager {
enabled: true,
},
});
let sourceDir = basename(dirname(__dirname)) === 'src' ? 'src' : 'dist';
const packageNames: string[] = items.map((item) => item.packageName);
const source = [];
for (const packageName of packageNames) {
const directory = join(packageName, sourceDir, 'server/commands/*.' + (sourceDir === 'src' ? 'ts' : 'js'));
source.push(directory);
const file = require.resolve(packageName);
const sourceDir = basename(dirname(file)) === 'src' ? 'src' : 'dist';
const directory = join(
packageName,
sourceDir,
'server/commands/*.' + (basename(dirname(file)) === 'src' ? 'ts' : 'js'),
);
source.push(directory.replaceAll(sep, '/'));
}
sourceDir = basename(dirname(__dirname)) === 'src' ? 'src' : 'lib';
for (const plugin of this.options.plugins || []) {
if (typeof plugin === 'string') {
const packageName = await PluginManager.getPackageName(plugin);
const file = require.resolve(packageName);
const sourceDir = basename(dirname(file)) === 'src' ? 'src' : 'lib';
const directory = join(packageName, sourceDir, 'server/commands/*.' + (sourceDir === 'src' ? 'ts' : 'js'));
source.push(directory);
source.push(directory.replaceAll(sep, '/'));
}
}
const files = await fg(source, {

View File

@ -12,8 +12,8 @@
"default": "./lib/index.js"
},
"import": {
"types": "./vitest.d.ts",
"default": "./vitest.mjs"
"types": "./es/index.d.ts",
"default": "./es/index.mjs"
}
},
"./client": {
@ -22,8 +22,8 @@
"default": "./lib/client/index.js"
},
"import": {
"types": "./lib/client/index.d.ts",
"default": "./lib/client/index.js"
"types": "./es/client/index.d.ts",
"default": "./es/client/index.mjs"
}
},
"./e2e": {
@ -32,11 +32,12 @@
"default": "./lib/e2e/index.js"
},
"import": {
"types": "./lib/e2e/index.d.ts",
"default": "./lib/e2e/index.js"
"types": "./es/e2e/index.d.ts",
"default": "./es/e2e/index.mjs"
}
},
"./package.json": "./package.json"
"./package.json": "./package.json",
"./vitest.mjs": "./vitest.mjs"
},
"dependencies": {
"@faker-js/faker": "8.1.0",
@ -45,6 +46,7 @@
"@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3",
"@testing-library/jest-dom": "^6.4.2",
"@types/supertest": "^2.0.11",
"@vitejs/plugin-react": "^4.0.0",
"jsdom": "^16.0.0",
@ -58,7 +60,6 @@
"supertest": "^6.1.6",
"vite": "^5.0.0",
"vitest": "^1.4.0",
"vitest-dom": "^0.1.1",
"ws": "^8.13.0"
},
"gitHead": "d0b4efe4be55f8c79a98a331d99d9f8cf99021a1"

View File

@ -1,7 +1,7 @@
import '@testing-library/jest-dom/vitest';
import { configure } from '@testing-library/react';
import dotenv from 'dotenv';
import { vi } from 'vitest';
import 'vitest-dom/extend-expect';
/**
* TypeError: URL.createObjectURL is not a function

View File

@ -1,22 +0,0 @@
declare global {
const suite: (typeof import('vitest'))['suite'];
const test: (typeof import('vitest'))['test'];
const describe: (typeof import('vitest'))['describe'];
const it: (typeof import('vitest'))['it'];
const expectTypeOf: (typeof import('vitest'))['expectTypeOf'];
const assertType: (typeof import('vitest'))['assertType'];
const expect: (typeof import('vitest'))['expect'];
const assert: (typeof import('vitest'))['assert'];
const vitest: (typeof import('vitest'))['vitest'];
const vi: (typeof import('vitest'))['vitest'];
const beforeAll: (typeof import('vitest'))['beforeAll'];
const afterAll: (typeof import('vitest'))['afterAll'];
const beforeEach: (typeof import('vitest'))['beforeEach'];
const afterEach: (typeof import('vitest'))['afterEach'];
}
import { type UserConfig } from 'vitest/config';
export declare const defineConfig: (
config?: UserConfig & {
server: boolean;
},
) => UserConfig;

View File

@ -1,2 +0,0 @@
// @ts-ignore
export * from '../vitest';

View File

@ -53,7 +53,6 @@ export interface CollectionSetting {
* @default false
*/
inherit?: boolean;
inherits?: string[];
category?: any[];
hidden?: boolean;
description?: string;
@ -120,6 +119,23 @@ interface AclRoleSetting {
key?: string;
//菜单权限配置
menuUiSchemas?: string[];
dataSourceKey?: string;
}
interface DatabaseSetting {
database: string;
host: string;
port: string;
schema?: string;
username?: string;
password?: string;
}
interface DataSourceSetting {
key: string;
displayName: string;
type: string;
options: DatabaseSetting;
enabled?: boolean;
}
export interface PageConfig {
@ -229,6 +245,15 @@ interface ExtendUtils {
*
*/
updateRole: <T = any>(roleSetting: AclRoleSetting) => Promise<T>;
/**
* pg
*/
mockExternalDataSource: <T = any>(DataSourceSetting: DataSourceSetting) => Promise<T>;
/**
*
* @param key key
*/
destoryExternalDataSource: <T = any>(key: string) => Promise<T>;
}
const PORT = process.env.APP_PORT || 20000;
@ -437,6 +462,20 @@ const _test = base.extend<ExtendUtils>({
await use(updateRole);
},
mockExternalDataSource: async ({ browser }, use) => {
const mockExternalDataSource = async (DataSourceSetting: DataSourceSetting) => {
return createExternalDataSource(DataSourceSetting);
};
await use(mockExternalDataSource);
},
destoryExternalDataSource: async ({ browser }, use) => {
const destoryDataSource = async (key: string) => {
return destoryExternalDataSource(key);
};
await use(destoryDataSource);
},
});
export const test = Object.assign(_test, {
@ -581,10 +620,6 @@ const deleteCollections = async (collectionNames: string[]) => {
const result = await api.post(`/api/collections:destroy?${params}`, {
headers,
params: {
// 自动删除依赖于集合的对象(如视图),进而删除依赖于这些对象的所有对象
cascade: true,
},
});
if (!result.ok()) {
@ -696,8 +731,12 @@ const updateRole = async (roleSetting: AclRoleSetting) => {
const state = await api.storageState();
const headers = getHeaders(state);
const name = roleSetting.name;
const dataSourceKey = roleSetting.dataSourceKey;
const result = await api.post(`/api/roles:update?filterByTk=${name}`, {
const url = !dataSourceKey
? `/api/roles:update?filterByTk=${name}`
: `/api/dataSources/${dataSourceKey}/roles:update?filterByTk=${name}`;
const result = await api.post(url, {
headers,
data: { ...roleSetting },
});
@ -724,7 +763,48 @@ const setDefaultRole = async (name) => {
data: { roleName: name },
});
};
/**
*
* @paramn
*/
const createExternalDataSource = async (dataSourceSetting: DataSourceSetting) => {
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
const state = await api.storageState();
const headers = getHeaders(state);
const result = await api.post(`/api/dataSources:create`, {
headers,
data: { ...dataSourceSetting },
});
if (!result.ok()) {
throw new Error(await result.text());
}
const dataSourceData = (await result.json()).data;
return dataSourceData;
};
/**
*
* @paramn
*/
const destoryExternalDataSource = async (key) => {
const api = await request.newContext({
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
});
const state = await api.storageState();
const headers = getHeaders(state);
const result = await api.post(`/api/dataSources:destroy?filterByTk=${key}`, {
headers,
});
if (!result.ok()) {
throw new Error(await result.text());
}
const dataSourceData = (await result.json()).data;
return dataSourceData;
};
/**
* collection Faker
* @param collectionSetting

Some files were not shown because too many files have changed in this diff Show More