Merge pull request 'merge_from_upstream' (#516) from merge_from_upstream into @hera/dev

Reviewed-on: daoyoucloud/tachycode#516
This commit is contained in:
sealday 2024-03-27 17:05:49 +08:00
commit 4023f6a23d
315 changed files with 6149 additions and 2523 deletions

View File

@ -13,6 +13,8 @@ on:
- 'packages/core/acl/**' - 'packages/core/acl/**'
- 'packages/core/actions/**' - 'packages/core/actions/**'
- 'packages/core/database/**' - 'packages/core/database/**'
- 'packages/core/resourcer/**'
- 'packages/core/data-source-manager/**'
- 'packages/core/server/**' - 'packages/core/server/**'
- 'packages/core/utils/**' - 'packages/core/utils/**'
- 'packages/plugins/**/src/server/**' - 'packages/plugins/**/src/server/**'
@ -23,6 +25,8 @@ on:
- 'packages/core/acl/**' - 'packages/core/acl/**'
- 'packages/core/actions/**' - 'packages/core/actions/**'
- 'packages/core/database/**' - 'packages/core/database/**'
- 'packages/core/resourcer/**'
- 'packages/core/data-source-manager/**'
- 'packages/core/server/**' - 'packages/core/server/**'
- 'packages/core/utils/**' - 'packages/core/utils/**'
- 'packages/plugins/**/src/server/**' - 'packages/plugins/**/src/server/**'

View File

@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
## [v0.20.0-alpha.17](https://github.com/nocobase/nocobase/compare/v0.20.0-alpha.16...v0.20.0-alpha.17) - 2024-03-26
### Merged
- feat: read pretty input number field support display format config [`#3815`](https://github.com/nocobase/nocobase/pull/3815)
- fix(Table): fix invalid pagination [`#3821`](https://github.com/nocobase/nocobase/pull/3821)
- chore: add tsdoc [`#3788`](https://github.com/nocobase/nocobase/pull/3788)
- chore(test): fix agent type [`#3819`](https://github.com/nocobase/nocobase/pull/3819)
- fix: embed plugin need hooks and e2e change [`#3727`](https://github.com/nocobase/nocobase/pull/3727)
- fix(associationBlock): fix association blocks for parent collection f… [`#3813`](https://github.com/nocobase/nocobase/pull/3813)
- fix(plugin-workflow-manual): fix schema migration [`#3814`](https://github.com/nocobase/nocobase/pull/3814)
- refactor(DataBlock): table block [`#3748`](https://github.com/nocobase/nocobase/pull/3748)
- fix(Details): block template [`#3807`](https://github.com/nocobase/nocobase/pull/3807)
- chore: cascade can replace set null action [`#3812`](https://github.com/nocobase/nocobase/pull/3812)
- feat(data-vi): support multiple data sources [`#3743`](https://github.com/nocobase/nocobase/pull/3743)
- feat(plugin-workflow): support multiple data source in workflow [`#3739`](https://github.com/nocobase/nocobase/pull/3739)
- chore: add options for matching and ignoring test files in e2e and p-test commands [`#3811`](https://github.com/nocobase/nocobase/pull/3811)
- chore: file collection template preset fields should be disabled [`#3810`](https://github.com/nocobase/nocobase/pull/3810)
- fix(plugin-workflow): remove string template in condition calculation [`#3688`](https://github.com/nocobase/nocobase/pull/3688)
- fix: refresh collection name when update [`#3797`](https://github.com/nocobase/nocobase/pull/3797)
- fix: reload when data source click refresh [`#3804`](https://github.com/nocobase/nocobase/pull/3804)
- fix: plugin manager keywords [`#3809`](https://github.com/nocobase/nocobase/pull/3809)
- fix: expand action and add new action should support drag & sort [`#3808`](https://github.com/nocobase/nocobase/pull/3808)
- fix: create attachments middleware [`#3794`](https://github.com/nocobase/nocobase/pull/3794)
- fix: useExpressionScope [`#3805`](https://github.com/nocobase/nocobase/pull/3805)
- chore: set default association reference on delete action to no action [`#3722`](https://github.com/nocobase/nocobase/pull/3722)
- fix: field permission all fields should be displayed [`#3799`](https://github.com/nocobase/nocobase/pull/3799)
### Commits
- chore(versions): 😊 publish v0.20.0-alpha.17 [`3398222`](https://github.com/nocobase/nocobase/commit/339822241f2f641656f64107318b793d63d0b2c9)
- fix: description [`0dc0d32`](https://github.com/nocobase/nocobase/commit/0dc0d329f80c268672bd80fc6cb0190c3cef964d)
- chore: update changelog [`35a6514`](https://github.com/nocobase/nocobase/commit/35a6514993bede12b952ce13641f7258fe6c76d2)
## [v0.20.0-alpha.16](https://github.com/nocobase/nocobase/compare/v0.20.0-alpha.15...v0.20.0-alpha.16) - 2024-03-23 ## [v0.20.0-alpha.16](https://github.com/nocobase/nocobase/compare/v0.20.0-alpha.15...v0.20.0-alpha.16) - 2024-03-23
### Merged ### Merged

View File

@ -1,5 +1,5 @@
{ {
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": ["--ignore-engines"], "npmClientArgs": ["--ignore-engines"],

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/acl", "name": "@nocobase/acl",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/resourcer": "0.20.0-alpha.16", "@nocobase/resourcer": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"minimatch": "^5.1.1" "minimatch": "^5.1.1"
}, },
"repository": { "repository": {

View File

@ -45,14 +45,40 @@ interface CanArgs {
} }
export class ACL extends EventEmitter { export class ACL extends EventEmitter {
/**
* @internal
*/
public availableStrategy = new Map<string, ACLAvailableStrategy>(); public availableStrategy = new Map<string, ACLAvailableStrategy>();
/**
* @internal
*/
public allowManager = new AllowManager(this); public allowManager = new AllowManager(this);
/**
* @internal
*/
public snippetManager = new SnippetManager(); public snippetManager = new SnippetManager();
/**
* @internal
*/
roles = new Map<string, ACLRole>(); roles = new Map<string, ACLRole>();
/**
* @internal
*/
actionAlias = new Map<string, string>(); actionAlias = new Map<string, string>();
/**
* @internal
*/
configResources: string[] = []; configResources: string[] = [];
protected availableActions = new Map<string, ACLAvailableAction>(); protected availableActions = new Map<string, ACLAvailableAction>();
protected fixedParamsManager = new FixedParamsManager(); protected fixedParamsManager = new FixedParamsManager();
protected middlewares: Toposort<any>; protected middlewares: Toposort<any>;
constructor() { constructor() {
@ -114,14 +140,23 @@ export class ACL extends EventEmitter {
return this.roles.delete(name); return this.roles.delete(name);
} }
/**
* @internal
*/
registerConfigResources(names: string[]) { registerConfigResources(names: string[]) {
names.forEach((name) => this.registerConfigResource(name)); names.forEach((name) => this.registerConfigResource(name));
} }
/**
* @internal
*/
registerConfigResource(name: string) { registerConfigResource(name: string) {
this.configResources.push(name); this.configResources.push(name);
} }
/**
* @internal
*/
isConfigResource(name: string) { isConfigResource(name: string) {
return this.configResources.includes(name); return this.configResources.includes(name);
} }
@ -227,6 +262,9 @@ export class ACL extends EventEmitter {
return null; return null;
} }
/**
* @internal
*/
public resolveActionAlias(action: string) { public resolveActionAlias(action: string) {
return this.actionAlias.get(action) ? this.actionAlias.get(action) : action; return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
} }
@ -242,6 +280,9 @@ export class ACL extends EventEmitter {
return this.skip(resourceName, actionNames, condition); return this.skip(resourceName, actionNames, condition);
} }
/**
* @deprecated
*/
skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) { skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
if (!Array.isArray(actionNames)) { if (!Array.isArray(actionNames)) {
actionNames = [actionNames]; actionNames = [actionNames];
@ -252,6 +293,9 @@ export class ACL extends EventEmitter {
} }
} }
/**
* @internal
*/
async parseJsonTemplate(json: any, ctx: any) { async parseJsonTemplate(json: any, ctx: any) {
if (json.filter) { if (json.filter) {
ctx.logger?.info?.('parseJsonTemplate.raw', JSON.parse(JSON.stringify(json.filter))); ctx.logger?.info?.('parseJsonTemplate.raw', JSON.parse(JSON.stringify(json.filter)));
@ -295,6 +339,9 @@ export class ACL extends EventEmitter {
}; };
} }
/**
* @internal
*/
async getActionParams(ctx) { async getActionParams(ctx) {
const roleName = ctx.state.currentRole || 'anonymous'; const roleName = ctx.state.currentRole || 'anonymous';
const { resourceName, actionName } = ctx.action; const { resourceName, actionName } = ctx.action;
@ -322,6 +369,9 @@ export class ACL extends EventEmitter {
this.snippetManager.register(snippet); this.snippetManager.register(snippet);
} }
/**
* @internal
*/
filterParams(ctx, resourceName, params) { filterParams(ctx, resourceName, params) {
if (params?.filter?.createdById) { if (params?.filter?.createdById) {
const collection = ctx.db.getCollection(resourceName); const collection = ctx.db.getCollection(resourceName);

View File

@ -1,14 +1,14 @@
{ {
"name": "@nocobase/actions", "name": "@nocobase/actions",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/cache": "0.20.0-alpha.16", "@nocobase/cache": "0.20.0-alpha.17",
"@nocobase/database": "0.20.0-alpha.16", "@nocobase/database": "0.20.0-alpha.17",
"@nocobase/resourcer": "0.20.0-alpha.16" "@nocobase/resourcer": "0.20.0-alpha.17"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,17 +1,17 @@
{ {
"name": "@nocobase/app", "name": "@nocobase/app",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/database": "0.20.0-alpha.16", "@nocobase/database": "0.20.0-alpha.17",
"@nocobase/preset-nocobase": "0.20.0-alpha.16", "@nocobase/preset-nocobase": "0.20.0-alpha.17",
"@nocobase/server": "0.20.0-alpha.16" "@nocobase/server": "0.20.0-alpha.17"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/client": "0.20.0-alpha.16" "@nocobase/client": "0.20.0-alpha.17"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/auth", "name": "@nocobase/auth",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "0.20.0-alpha.16", "@nocobase/actions": "0.20.0-alpha.17",
"@nocobase/cache": "0.20.0-alpha.16", "@nocobase/cache": "0.20.0-alpha.17",
"@nocobase/database": "0.20.0-alpha.16", "@nocobase/database": "0.20.0-alpha.17",
"@nocobase/resourcer": "0.20.0-alpha.16", "@nocobase/resourcer": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/build", "name": "@nocobase/build",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "Library build tool based on rollup.", "description": "Library build tool based on rollup.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",

View File

@ -14,16 +14,22 @@ import { buildClient } from './buildClient';
import { buildCjs } from './buildCjs'; import { buildCjs } from './buildCjs';
import { buildPlugin } from './buildPlugin'; import { buildPlugin } from './buildPlugin';
import { buildDeclaration } from './buildDeclaration'; import { buildDeclaration } from './buildDeclaration';
import { PkgLog, getPkgLog, toUnixPath, getPackageJson, getUserConfig, UserConfig } from './utils'; import { PkgLog, getPkgLog, toUnixPath, getPackageJson, getUserConfig, UserConfig, writeToCache, readFromCache } from './utils';
import { getPackages } from './utils/getPackages'; import { getPackages } from './utils/getPackages';
import { Package } from '@lerna/package'; import { Package } from '@lerna/package';
import { tarPlugin } from './tarPlugin' import { tarPlugin } from './tarPlugin'
const BUILD_ERROR = 'build-error';
export async function build(pkgs: string[]) { export async function build(pkgs: string[]) {
const isDev = process.argv.includes('--development'); const isDev = process.argv.includes('--development');
process.env.NODE_ENV = isDev ? 'development' : 'production'; process.env.NODE_ENV = isDev ? 'development' : 'production';
const packages = getPackages(pkgs); let packages = getPackages(pkgs);
const cachePkg = readFromCache(BUILD_ERROR);
if (process.argv.includes('--retry') && cachePkg?.pkg) {
packages = packages.slice(packages.findIndex((item) => item.name === cachePkg.pkg));
}
if (packages.length === 0) { if (packages.length === 0) {
let msg = ''; let msg = '';
if (pkgs.length) { if (pkgs.length) {
@ -59,6 +65,7 @@ export async function build(pkgs: string[]) {
APP_ROOT: path.join(CORE_APP, 'client'), APP_ROOT: path.join(CORE_APP, 'client'),
}); });
} }
writeToCache(BUILD_ERROR, {});
} }
export async function buildPackages( export async function buildPackages(
@ -67,6 +74,7 @@ export async function buildPackages(
doBuildPackage: (cwd: string, userConfig: UserConfig, sourcemap: boolean, log?: PkgLog) => Promise<any>, doBuildPackage: (cwd: string, userConfig: UserConfig, sourcemap: boolean, log?: PkgLog) => Promise<any>,
) { ) {
for await (const pkg of packages) { for await (const pkg of packages) {
writeToCache(BUILD_ERROR, { pkg: pkg.name })
await buildPackage(pkg, targetDir, doBuildPackage); await buildPackage(pkg, targetDir, doBuildPackage);
} }
} }

View File

@ -26,6 +26,7 @@ export const EsbuildSupportExts = [
'.data', '.data',
]; ];
export const ROOT_PATH = path.join(__dirname, '../../../../'); export const ROOT_PATH = path.join(__dirname, '../../../../');
export const NODE_MODULES = path.join(ROOT_PATH, 'node_modules');
export const PACKAGES_PATH = path.join(ROOT_PATH, 'packages'); export const PACKAGES_PATH = path.join(ROOT_PATH, 'packages');
export const PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins'] export const PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins']
.concat((process.env.PLUGINS_DIRS || '').split(',')) .concat((process.env.PLUGINS_DIRS || '').split(','))

View File

@ -10,6 +10,7 @@ import { toUnixPath } from './utils';
* npm * npm
* @example * @example
* yarn build packages/core/client @nocobase/acl => ['/home/xx/packages/core/client', '/home/xx/packages/core/acl'] * yarn build packages/core/client @nocobase/acl => ['/home/xx/packages/core/client', '/home/xx/packages/core/acl']
* yarn build packages/plugins/* => ['/home/xx/packages/plugins/a', '/home/xx/packages/plugins/b']
* yarn build => all packages * yarn build => all packages
*/ */
function getPackagesPath(pkgs: string[]) { function getPackagesPath(pkgs: string[]) {
@ -24,7 +25,6 @@ function getPackagesPath(pkgs: string[]) {
return allPackageJson return allPackageJson
.map(toUnixPath).map(item => path.dirname(item)); .map(toUnixPath).map(item => path.dirname(item));
} }
const allPackageInfo = allPackageJson const allPackageInfo = allPackageJson
.map(packageJsonPath => ({ name: require(packageJsonPath).name, path: path.dirname(toUnixPath(packageJsonPath)) })) .map(packageJsonPath => ({ name: require(packageJsonPath).name, path: path.dirname(toUnixPath(packageJsonPath)) }))
.reduce((acc, cur) => { .reduce((acc, cur) => {
@ -37,7 +37,9 @@ function getPackagesPath(pkgs: string[]) {
const relativePaths = pkgNames.length ? pkgs.filter(item => !pkgNames.includes(item)) : pkgs; const relativePaths = pkgNames.length ? pkgs.filter(item => !pkgNames.includes(item)) : pkgs;
const pkgPaths = pkgs.map(item => allPackageInfo[item]) const pkgPaths = pkgs.map(item => allPackageInfo[item])
const absPaths = allPackagePaths.filter(absPath => relativePaths.some((relativePath) => absPath.endsWith(relativePath))); const absPaths = allPackagePaths.filter(absPath => relativePaths.some((relativePath) => absPath.endsWith(relativePath)));
return [...pkgPaths, ...absPaths]; const dirPaths = fg.sync(pkgs, { onlyDirectories: true, absolute: true, cwd: ROOT_PATH });
const dirMatchPaths = allPackagePaths.filter(pkgPath => dirPaths.some(dirPath => pkgPath.startsWith(dirPath)));
return [...new Set([...pkgPaths, ...absPaths, ...dirMatchPaths])];
} }
export function getPackages(pkgs: string[]) { export function getPackages(pkgs: string[]) {

View File

@ -1,10 +1,11 @@
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import fs from 'fs-extra';
import fg from 'fast-glob'; import fg from 'fast-glob';
import fs from 'fs-extra';
import { Options as TsupConfig } from 'tsup' import { Options as TsupConfig } from 'tsup'
import { InlineConfig as ViteConfig } from 'vite' import { InlineConfig as ViteConfig } from 'vite'
import { register } from 'esbuild-register/dist/node'; import { register } from 'esbuild-register/dist/node';
import { NODE_MODULES } from '../constant';
let previousColor = ''; let previousColor = '';
function randomColor() { function randomColor() {
@ -79,3 +80,18 @@ export function getUserConfig(cwd: string) {
} }
return config; return config;
} }
const CACHE_DIR = path.join(NODE_MODULES, '.cache', 'nocobase');
export function writeToCache(key: string, data: Record<string, any>) {
const cachePath = path.join(CACHE_DIR, `${key}.json`);
fs.ensureDirSync(path.dirname(cachePath));
fs.writeJsonSync(cachePath, data, { spaces: 2 });
}
export function readFromCache(key: string) {
const cachePath = path.join(CACHE_DIR, `${key}.json`);
if (fs.existsSync(cachePath)) {
return fs.readJsonSync(cachePath);
}
return {};
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cache", "name": "@nocobase/cache",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/cli", "name": "@nocobase/cli",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./src/index.js", "main": "./src/index.js",
@ -8,7 +8,7 @@
"nocobase": "./bin/index.js" "nocobase": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"@nocobase/app": "0.20.0-alpha.16", "@nocobase/app": "0.20.0-alpha.17",
"@types/fs-extra": "^11.0.1", "@types/fs-extra": "^11.0.1",
"@umijs/utils": "3.5.20", "@umijs/utils": "3.5.20",
"chalk": "^4.1.1", "chalk": "^4.1.1",
@ -25,7 +25,7 @@
"tsx": "^4.6.2" "tsx": "^4.6.2"
}, },
"devDependencies": { "devDependencies": {
"@nocobase/devtools": "0.20.0-alpha.16" "@nocobase/devtools": "0.20.0-alpha.17"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -13,6 +13,7 @@ module.exports = (cli) => {
.argument('[packages...]') .argument('[packages...]')
.option('-v, --version', 'print version') .option('-v, --version', 'print version')
.option('-c, --compile', 'compile the @nocobase/build package') .option('-c, --compile', 'compile the @nocobase/build package')
.option('-r, --retry', 'retry the last failed package')
.option('-w, --watch', 'watch compile the @nocobase/build package') .option('-w, --watch', 'watch compile the @nocobase/build package')
.option('-s, --sourcemap', 'generate sourcemap') .option('-s, --sourcemap', 'generate sourcemap')
.option('--no-dts', 'not generate dts') .option('--no-dts', 'not generate dts')
@ -24,12 +25,14 @@ module.exports = (cli) => {
}); });
if (options.watch) return; if (options.watch) return;
} }
process.env['VITE_CJS_IGNORE_WARNING'] = 'true';
await run('nocobase-build', [ await run('nocobase-build', [
...pkgs, ...pkgs,
options.version ? '--version' : '', options.version ? '--version' : '',
!options.dts ? '--no-dts' : '', !options.dts ? '--no-dts' : '',
options.sourcemap ? '--sourcemap' : '', options.sourcemap ? '--sourcemap' : '',
options.retry ? '--retry' : '',
]); ]);
buildIndexHtml(true); buildIndexHtml(true);
}); });

View File

@ -243,6 +243,12 @@ module.exports = (cli) => {
.option('--stop-on-error') .option('--stop-on-error')
.option('--build') .option('--build')
.option('--concurrency [concurrency]', '', os.cpus().length) .option('--concurrency [concurrency]', '', os.cpus().length)
.option(
'--match [match]',
'Only the files matching one of these patterns are executed as test files. Matching is performed against the absolute file path. Strings are treated as glob patterns.',
'packages/**/__e2e__/**/*.test.ts',
)
.option('--ignore [ignore]', 'Skip tests that match the pattern. Strings are treated as glob patterns.', undefined)
.action(async (options) => { .action(async (options) => {
process.env.__E2E__ = true; process.env.__E2E__ = true;
if (options.build) { if (options.build) {

View File

@ -60,7 +60,8 @@ exports.pTest = async (options) => {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
} }
const files = glob.sync('packages/**/__e2e__/**/*.test.ts', { const files = glob.sync(options.match, {
ignore: options.ignore,
root: process.cwd(), root: process.cwd(),
}); });

View File

@ -19,6 +19,7 @@ function addTestCommand(name, cli) {
.arguments('[paths...]') .arguments('[paths...]')
.allowUnknownOption() .allowUnknownOption()
.action(async (paths, opts) => { .action(async (paths, opts) => {
process.argv.push('--disable-console-intercept');
if (name === 'test:server') { if (name === 'test:server') {
process.env.TEST_ENV = 'server-side'; process.env.TEST_ENV = 'server-side';
} else if (name === 'test:client') { } else if (name === 'test:client') {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/client", "name": "@nocobase/client",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "lib/index.js", "main": "lib/index.js",
"module": "es/index.mjs", "module": "es/index.mjs",
@ -26,9 +26,9 @@
"@formily/reactive-react": "^2.2.27", "@formily/reactive-react": "^2.2.27",
"@formily/shared": "^2.2.27", "@formily/shared": "^2.2.27",
"@formily/validator": "^2.2.27", "@formily/validator": "^2.2.27",
"@nocobase/evaluators": "0.20.0-alpha.16", "@nocobase/evaluators": "0.20.0-alpha.17",
"@nocobase/sdk": "0.20.0-alpha.16", "@nocobase/sdk": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"ahooks": "^3.7.2", "ahooks": "^3.7.2",
"antd": "^5.12.8", "antd": "^5.12.8",
"antd-style": "3.4.5", "antd-style": "3.4.5",

View File

@ -11,7 +11,7 @@ export const ADMIN_SETTINGS_PATH = '/admin/settings/';
export const SNIPPET_PREFIX = 'pm.'; export const SNIPPET_PREFIX = 'pm.';
export interface PluginSettingOptions { export interface PluginSettingOptions {
title: string | React.ReactElement; title: any;
/** /**
* @default Outlet * @default Outlet
*/ */

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component'; import { SchemaComponent, SchemaComponentProvider } from '../../../schema-component';
import { render, screen, sleep, userEvent, waitFor } from '@nocobase/test/client'; import { render } from '@nocobase/test/client';
import { withDynamicSchemaProps } from '../../hoc'; import { withDynamicSchemaProps } from '../../hoc';
const HelloComponent = withDynamicSchemaProps((props: any) => ( const HelloComponent = withDynamicSchemaProps((props: any) => (
@ -66,7 +66,7 @@ describe('withDynamicSchemaProps', () => {
const Demo = withTestDemo(schema, scopes); const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />); const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ b: 'b', a: 'a' }));
}); });
test('x-use-decorator-props', () => { test('x-use-decorator-props', () => {
@ -101,7 +101,7 @@ describe('withDynamicSchemaProps', () => {
const Demo = withTestDemo(schema, scopes); const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />); const { getByTestId } = render(<Demo />);
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ b: 'b', a: 'a' }));
}); });
test('x-use-component-props and x-use-decorator-props exist simultaneously', () => { test('x-use-component-props and x-use-decorator-props exist simultaneously', () => {
@ -130,8 +130,8 @@ describe('withDynamicSchemaProps', () => {
const Demo = withTestDemo(schema, scopes); const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />); const { getByTestId } = render(<Demo />);
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' })); expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ b: 'b', a: 'a' }));
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ c: 'c', d: 'd' })); expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ d: 'd', c: 'c' }));
}); });
test('no register scope', () => { test('no register scope', () => {
@ -142,4 +142,23 @@ describe('withDynamicSchemaProps', () => {
const { getByTestId } = render(<Demo />); const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({})); expect(getByTestId('component')).toHaveTextContent(JSON.stringify({}));
}); });
test('x-use-component-props should override x-component-props', () => {
function useComponentProps() {
return {
a: 'a',
};
}
const schema = {
'x-use-component-props': 'useComponentProps',
'x-component-props': {
a: 'b',
},
};
const scopes = { useComponentProps };
const Demo = withTestDemo(schema, scopes);
const { getByTestId } = render(<Demo />);
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ a: 'a' }));
});
}); });

View File

@ -1,6 +1,7 @@
import { useExpressionScope } from '@formily/react';
import { merge, omit } from 'lodash'; import { merge, omit } from 'lodash';
import React, { ComponentType, useMemo } from 'react'; import React, { ComponentType, useMemo } from 'react';
import { useDesignable, useSchemaComponentContext } from '../../schema-component'; import { useDesignable } from '../../schema-component';
const useDefaultSchemaProps = () => undefined; const useDefaultSchemaProps = () => undefined;
@ -8,11 +9,11 @@ interface WithSchemaHookOptions {
displayName?: string; displayName?: string;
} }
export function withDynamicSchemaProps<T = any>(Component: ComponentType<T>, options: WithSchemaHookOptions = {}) { export function withDynamicSchemaProps<T = any>(Component: any, options: WithSchemaHookOptions = {}) {
const displayName = options.displayName || Component.displayName || Component.name; const displayName = options.displayName || Component.displayName || Component.name;
const ComponentWithProps: ComponentType<T> = (props) => { const ComponentWithProps: ComponentType<T> = (props) => {
const { dn, findComponent } = useDesignable(); const { dn, findComponent } = useDesignable();
const { scope } = useSchemaComponentContext(); const scope = useExpressionScope();
const useComponentPropsStr = useMemo(() => { const useComponentPropsStr = useMemo(() => {
const xComponent = dn.getSchemaAttribute('x-component'); const xComponent = dn.getSchemaAttribute('x-component');
const xDecorator = dn.getSchemaAttribute('x-decorator'); const xDecorator = dn.getSchemaAttribute('x-decorator');
@ -40,7 +41,7 @@ export function withDynamicSchemaProps<T = any>(Component: ComponentType<T>, opt
const schemaProps = useSchemaProps(props); const schemaProps = useSchemaProps(props);
const memoProps = useMemo(() => { const memoProps = useMemo(() => {
return merge(omit(schemaProps, 'children'), omit(props, 'children')); return merge(omit(props, 'children'), omit(schemaProps, 'children'));
}, [schemaProps, props]); }, [schemaProps, props]);
return <Component {...memoProps}>{props.children}</Component>; return <Component {...memoProps}>{props.children}</Component>;

View File

@ -59,31 +59,6 @@ export const useBlockResource = () => {
return useContext(BlockResourceContext) || resource; return useContext(BlockResourceContext) || resource;
}; };
interface UseResourceProps {
resource: any;
association?: any;
useSourceId?: any;
collection?: any;
dataSource?: any;
block?: any;
}
const useAssociation = (props) => {
const { association } = props;
const { getCollectionField } = useCollectionManager_deprecated();
if (typeof association === 'string') {
return getCollectionField(association);
} else if (association?.collectionName && association?.name) {
return getCollectionField(`${association?.collectionName}.${association?.name}`);
}
};
const useActionParams = (props) => {
const { useParams } = props;
const params = useParams?.() || {};
return { ...props.params, ...params };
};
export const MaybeCollectionProvider = (props) => { export const MaybeCollectionProvider = (props) => {
const { collection } = props; const { collection } = props;
return collection ? ( return collection ? (
@ -218,6 +193,22 @@ export const useBlockContext = () => {
return useContext(BlockContext); return useContext(BlockContext);
}; };
/**
* Schema
*/
const useCompatDataBlockSourceId = (props) => {
const fieldSchema = useFieldSchema();
// 如果存在 x-use-decorator-props说明是新版 Schema
if (fieldSchema['x-use-decorator-props']) {
return props.sourceId;
} else {
// 是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks
// eslint-disable-next-line react-hooks/rules-of-hooks
return useDataBlockSourceId(props);
}
};
/** /**
* @deprecated use `DataBlockProvider` instead * @deprecated use `DataBlockProvider` instead
*/ */
@ -236,8 +227,11 @@ export const BlockProvider = (props: {
useParams?: any; useParams?: any;
}) => { }) => {
const { name, dataSource, association, useParams, parentRecord } = props; const { name, dataSource, association, useParams, parentRecord } = props;
const sourceId = useDataBlockSourceId({ association }); const sourceId = useCompatDataBlockSourceId(props);
// 新版1.0)已弃用 useParams这里之所以继续保留是为了兼容旧版的 UISchema
const paramsFromHook = useParams?.(); const paramsFromHook = useParams?.();
const { getAssociationAppends } = useAssociationNames(dataSource); const { getAssociationAppends } = useAssociationNames(dataSource);
const { appends, updateAssociationValues } = getAssociationAppends(); const { appends, updateAssociationValues } = getAssociationAppends();
const params = useMemo(() => { const params = useMemo(() => {
@ -330,7 +324,9 @@ export const useParamsFromRecord = () => {
const { fields } = useCollection_deprecated(); const { fields } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getCollectionJoinField } = useCollectionManager_deprecated(); const { getCollectionJoinField } = useCollectionManager_deprecated();
const collectionField = getCollectionJoinField(fieldSchema?.['x-decorator-props']?.resource); const collectionField = getCollectionJoinField(
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association,
);
const filterFields = fields const filterFields = fields
.filter((v) => { .filter((v) => {
return ['boolean', 'date', 'integer', 'radio', 'sort', 'string', 'time', 'uid', 'uuid'].includes(v.type); return ['boolean', 'date', 'integer', 'radio', 'sort', 'string', 'time', 'uid', 'uuid'].includes(v.type);

View File

@ -10,11 +10,13 @@ import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvid
import { FilterFormBlockProvider } from './FilterFormBlockProvider'; import { FilterFormBlockProvider } from './FilterFormBlockProvider';
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider'; import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider'; import { FormFieldProvider, useFormFieldProps } from './FormFieldProvider';
import { TableBlockProvider, useTableBlockProps } from './TableBlockProvider'; import { TableBlockProvider } from './TableBlockProvider';
import { useTableBlockProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockProps';
import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider'; import { TableFieldProvider, useTableFieldProps } from './TableFieldProvider';
import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider'; import { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
import * as bp from './hooks'; import * as bp from './hooks';
import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar'; import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar';
import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
// TODO: delete this, replaced by `BlockSchemaComponentPlugin` // TODO: delete this, replaced by `BlockSchemaComponentPlugin`
export const BlockSchemaComponentProvider: React.FC = (props) => { export const BlockSchemaComponentProvider: React.FC = (props) => {
@ -41,6 +43,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
useTableFieldProps, useTableFieldProps,
useTableBlockProps, useTableBlockProps,
useTableSelectorProps, useTableSelectorProps,
useTableBlockDecoratorProps,
}} }}
> >
{props.children} {props.children}
@ -84,6 +87,7 @@ export class BlockSchemaComponentPlugin extends Plugin {
useTableFieldProps, useTableFieldProps,
useTableBlockProps, useTableBlockProps,
useTableSelectorProps, useTableSelectorProps,
useTableBlockDecoratorProps,
}); });
} }
} }

View File

@ -1,12 +1,12 @@
import { ArrayField, createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormContext, useField, useFieldSchema } from '@formily/react'; import { FormContext, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useContext, useMemo, useState } from 'react';
import { useCollectionManager_deprecated } from '../collection-manager'; import { useCollectionManager_deprecated } from '../collection-manager';
import { useFilterBlock } from '../filter-provider/FilterProvider'; import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
import { mergeFilter } from '../filter-provider/utils';
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { findFilterTargets, useParsedFilter } from './hooks'; import { useParsedFilter } from './hooks';
import { useTableBlockParams } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
import { withDynamicSchemaProps } from '../application/hoc/withDynamicSchemaProps';
export const TableBlockContext = createContext<any>({}); export const TableBlockContext = createContext<any>({});
TableBlockContext.displayName = 'TableBlockContext'; TableBlockContext.displayName = 'TableBlockContext';
@ -74,16 +74,37 @@ const InternalTableBlockProvider = (props: Props) => {
); );
}; };
export const TableBlockProvider = (props) => { /**
const resourceName = props.resource; * schema
const params = useMemo(() => ({ ...props.params }), [props.params]); * @param props
* @returns
*/
const useTableBlockParamsCompat = (props) => {
const fieldSchema = useFieldSchema();
let params;
// 1. 新版本的 schema 存在 x-use-decorator-props 属性
if (fieldSchema['x-use-decorator-props']) {
params = props.params;
} else {
// 2. 旧版本的 schema 不存在 x-use-decorator-props 属性
// 因为 schema 中是否存在 x-use-decorator-props 是固定不变的,所以这里可以使用 hooks
// eslint-disable-next-line react-hooks/rules-of-hooks
params = useTableBlockParams(props);
}
return params;
};
export const TableBlockProvider = withDynamicSchemaProps((props) => {
const resourceName = props.resource || props.association;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource); const { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource);
const collection = getCollection(props.collection, props.dataSource); const collection = getCollection(props.collection, props.dataSource);
const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {}; const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
if (props.dragSort && dragSortBy) { const params = useTableBlockParamsCompat(props);
params['sort'] = dragSortBy;
}
let childrenColumnName = 'children'; let childrenColumnName = 'children';
if (collection?.tree && treeTable !== false) { if (collection?.tree && treeTable !== false) {
if (resourceName?.includes('.')) { if (resourceName?.includes('.')) {
@ -101,140 +122,18 @@ export const TableBlockProvider = (props) => {
} }
} }
const form = useMemo(() => createForm(), [treeTable]); const form = useMemo(() => createForm(), [treeTable]);
const { filter: parsedFilter } = useParsedFilter({
filterOption: params?.filter,
});
const paramsWithFilter = useMemo(() => {
return {
...params,
filter: parsedFilter,
};
}, [parsedFilter, params]);
return ( return (
<SchemaComponentOptions scope={{ treeTable }}> <SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}> <FormContext.Provider value={form}>
<BlockProvider name={props.name || 'table'} {...props} params={paramsWithFilter} runWhenParamsChanged> <BlockProvider name={props.name || 'table'} {...props} params={params} runWhenParamsChanged>
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} /> <InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
</BlockProvider> </BlockProvider>
</FormContext.Provider> </FormContext.Provider>
</SchemaComponentOptions> </SchemaComponentOptions>
); );
}; });
export const useTableBlockContext = () => { export const useTableBlockContext = () => {
return useContext(TableBlockContext); return useContext(TableBlockContext);
}; };
export const useTableBlockProps = () => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const ctx = useTableBlockContext();
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
const { getDataBlocks } = useFilterBlock();
useEffect(() => {
if (!ctx?.service?.loading) {
field.value = [];
field.value = ctx?.service?.data?.data;
field?.setInitialValue(ctx?.service?.data?.data);
field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
field.componentProps.pagination = field.componentProps.pagination || {};
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
}
}, [ctx?.service?.data, ctx?.service?.loading]); // 这里如果依赖了 ctx?.field?.data?.selectedRowKeys 的话,会导致这个问题:
return {
childrenColumnName: ctx.childrenColumnName,
loading: ctx?.service?.loading,
showIndex: ctx.showIndex,
dragSort: ctx.dragSort && ctx.dragSortBy,
rowKey: ctx.rowKey || 'id',
pagination:
ctx?.params?.paginate !== false
? {
defaultCurrent: ctx?.params?.page || 1,
defaultPageSize: ctx?.params?.pageSize,
}
: false,
onRowSelectionChange(selectedRowKeys) {
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx?.field?.onRowSelect?.(selectedRowKeys);
},
async onRowDragEnd({ from, to }) {
await ctx.resource.move({
sourceId: from[ctx.rowKey || 'id'],
targetId: to[ctx.rowKey || 'id'],
sortField: ctx.dragSort && ctx.dragSortBy,
});
ctx.service.refresh();
},
onChange({ current, pageSize }, filters, sorter) {
const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort;
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort });
},
onClickRow(record, setSelectedRow, selectedRow) {
const { targets, uid } = findFilterTargets(fieldSchema);
const dataBlocks = getDataBlocks();
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
setSelectedRow((prev) => (prev.length ? [] : prev));
return;
}
const value = [record[ctx.rowKey]];
dataBlocks.forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (selectedRow.includes(record[ctx.rowKey])) {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[uid];
} else {
storedFilter[uid] = {
$and: [
{
[target.field || ctx.rowKey]: {
[target.field ? '$in' : '$eq']: value,
},
},
],
};
}
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
block.defaultFilter,
]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
// 更新表格的选中状态
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
},
onExpand(expanded, record) {
ctx?.field.onExpandClick?.(expanded, record);
},
};
};

View File

@ -1,5 +1,10 @@
import { useFieldSchema } from '@formily/react'; import {
import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..'; InheritanceCollectionMixin,
useCollection,
useCollectionManager,
useCollectionParentRecordData,
useCollectionRecordData,
} from '../..';
/** /**
* schema sourceId * schema sourceId
@ -12,14 +17,17 @@ export const useDataBlockSourceId = ({ association }: { association: string }) =
const recordData = useCollectionRecordData(); const recordData = useCollectionRecordData();
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const cm = useCollectionManager(); const cm = useCollectionManager();
const collectionOutsideBlock = useCollection(); const collectionOutsideBlock = useCollection<InheritanceCollectionMixin>();
if (!association) return; if (!association) return;
const associationField = cm.getCollectionField(association); const associationField = cm.getCollectionField(association);
const associationCollection = cm.getCollection(associationField.collectionName); const associationCollection = cm.getCollection<InheritanceCollectionMixin>(associationField.collectionName);
if (collectionOutsideBlock.name === associationCollection.name) { if (
collectionOutsideBlock.name === associationCollection.name ||
collectionOutsideBlock.getParentCollectionsName?.().includes(associationCollection.name)
) {
return recordData?.[ return recordData?.[
associationField.sourceKey || associationField.sourceKey ||
associationCollection.filterTargetKey || associationCollection.filterTargetKey ||

View File

@ -193,6 +193,10 @@ export const PresetFields = observer(
rowSelection={{ rowSelection={{
type: 'checkbox', type: 'checkbox',
selectedRowKeys, selectedRowKeys,
getCheckboxProps: (record) => ({
disabled: form.values.template === 'file', // Column configuration not to be checked
name: record.name,
}),
onChange: (_, selectedRows) => { onChange: (_, selectedRows) => {
const fields = getDefaultCollectionFields(selectedRows, form.values); const fields = getDefaultCollectionFields(selectedRows, form.values);
const config = { const config = {

View File

@ -11,6 +11,7 @@ export interface DataSourceOptions {
collections?: CollectionOptions[]; collections?: CollectionOptions[];
errorMessage?: string; errorMessage?: string;
status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading'; status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading';
isDBInstance?: boolean;
} }
export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource; export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource;

View File

@ -910,5 +910,14 @@
"Second": "秒", "Second": "秒",
"Unix Timestamp": "Unix 时间戳", "Unix Timestamp": "Unix 时间戳",
"Field value do not meet the requirements": "字符不符合要求", "Field value do not meet the requirements": "字符不符合要求",
"Field value size is": "字符长度要求" "Field value size is": "字符长度要求",
"Style": "风格",
"Unit conversion": "单位换算",
"Separator": "分隔符",
"Prefix": "前缀",
"Suffix": "后缀",
"Multiply by":"乘以",
"Divide by":"除以",
"Scientifix notation":"科学计数法",
"Normal":"常规"
} }

View File

@ -177,7 +177,8 @@ export const multiDataDetailsBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Details', componentName: 'Details',
collectionName: name, collectionName: name,

View File

@ -16,7 +16,8 @@ export const singleDataDetailsBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
insertAdjacentPosition: 'beforeEnd', insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem', componentName: 'ReadPrettyFormItem',

View File

@ -51,7 +51,8 @@ export const creationFormBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: 'FormItem',
collectionName: name, collectionName: name,
@ -117,7 +118,8 @@ export const createFormBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: 'FormItem',
collectionName: name, collectionName: name,

View File

@ -50,7 +50,8 @@ export const editFormBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: 'FormItem',
collectionName: name, collectionName: name,

View File

@ -257,7 +257,8 @@ export const gridCardBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'GridCard', componentName: 'GridCard',

View File

@ -196,7 +196,8 @@ export const listBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'List', componentName: 'List',

View File

@ -2,9 +2,9 @@ import { TableOutlined } from '@ant-design/icons';
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context'; import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context';
import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated'; import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated';
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer'; import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
import { createTableBlockSchema } from '../../../../schema-initializer/utils';
import React from 'react'; import React from 'react';
import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection'; import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection';
import { createTableBlockUISchema } from './createTableBlockUISchema';
export const TableBlockInitializer = ({ export const TableBlockInitializer = ({
filterCollections, filterCollections,
@ -42,8 +42,8 @@ export const TableBlockInitializer = ({
} }
const collection = getCollection(item.name, item.dataSource); const collection = getCollection(item.name, item.dataSource);
const schema = createTableBlockSchema({ const schema = createTableBlockUISchema({
collection: item.name, collectionName: item.name,
dataSource: item.dataSource, dataSource: item.dataSource,
rowKey: collection.filterTargetKey || 'id', rowKey: collection.filterTargetKey || 'id',
}); });

View File

@ -1,4 +1,5 @@
import { createBlockInPage, expect, oneEmptyTable, test } from '@nocobase/test/e2e'; import { createBlockInPage, expect, oneEmptyTable, test } from '@nocobase/test/e2e';
import { T3686 } from './templatesOfBug';
test.describe('where table block can be added', () => { test.describe('where table block can be added', () => {
test('page', async ({ page, mockPage }) => { test('page', async ({ page, mockPage }) => {
@ -9,7 +10,45 @@ test.describe('where table block can be added', () => {
await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible(); await expect(page.getByLabel('block-item-CardItem-users-table')).toBeVisible();
}); });
test('popup', async () => {}); test('popup', async ({ page, mockPage, mockRecord }) => {
await mockPage(T3686).goto();
await mockRecord('parentCollection');
const childRecord = await mockRecord('childCollection');
// 打开弹窗
await page.getByLabel('action-Action.Link-View').click();
// 添加当前表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'childAssociationField ->' }).click();
await page
.getByTestId('drawer-Action.Container-childCollection-View record')
.getByLabel('schema-initializer-TableV2-')
.hover();
await page.getByRole('menuitem', { name: 'childTargetText' }).click();
// 添加父表关系区块
await page.getByLabel('schema-initializer-Grid-popup').hover();
await page.getByRole('menuitem', { name: 'table Table right' }).hover();
await page.getByRole('menuitem', { name: 'parentAssociationField ->' }).click();
await page.getByLabel('schema-initializer-TableV2-table:configureColumns-parentTargetCollection').hover();
await page.getByRole('menuitem', { name: 'parentTargetText' }).click();
// 普通关系区块应该显示正常
await expect(
page
.getByLabel('block-item-CardItem-childTargetCollection-table')
.getByText(childRecord.childAssociationField[0].childTargetText),
).toBeVisible();
// 父表关系区块应该显示正常
await expect(
page
.getByLabel('block-item-CardItem-parentTargetCollection-table')
.getByText(childRecord.parentAssociationField[0].parentTargetText),
).toBeVisible();
});
}); });
test.describe('configure actions', () => { test.describe('configure actions', () => {

View File

@ -1,3 +1,5 @@
import { PageConfig } from '@nocobase/test/e2e';
export const T2183 = { export const T2183 = {
pageSchema: { pageSchema: {
_isJSONSchemaObject: true, _isJSONSchemaObject: true,
@ -670,3 +672,260 @@ export const T2187 = {
'x-index': 1, 'x-index': 1,
}, },
}; };
export const T3686: PageConfig = {
collections: [
{
name: 'parentTargetCollection',
fields: [
{
name: 'parentTargetText',
interface: 'input',
},
],
},
{
name: 'childTargetCollection',
fields: [
{
name: 'childTargetText',
interface: 'input',
},
],
},
{
name: 'childCollection',
inherits: ['parentCollection'],
fields: [
{
name: 'childAssociationField',
interface: 'm2m',
target: 'childTargetCollection',
},
],
},
{
name: 'parentCollection',
fields: [
{
name: 'parentAssociationField',
interface: 'm2m',
target: 'parentTargetCollection',
},
],
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
fa2wzem9pud: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'page:addBlock',
properties: {
'14kr5bu1min': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
uc0jyubx2p3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
k22vt5rvlf8: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 'childCollection:list',
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: 'childCollection',
dataSource: 'main',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
'x-uid': '42xfns3215b',
'x-async': false,
'x-index': 1,
},
mv0fpgnz9x4: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'table:configureItemActions',
properties: {
'4pr5w722wko': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
properties: {
'0z54f36l29g': {
'x-uid': '6ga7ofdmqac',
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: 'View record',
'x-action': 'view',
'x-toolbar': 'ActionSchemaToolbar',
'x-settings': 'actionSettings:view',
'x-component': 'Action.Link',
'x-component-props': {
openMode: 'drawer',
danger: false,
},
'x-decorator': 'ACLActionProvider',
'x-designer-props': {
linkageAction: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("View record") }}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializers',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Details")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'popup:common:addBlock',
'x-uid': '8isg655oydv',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'avctzq7wpne',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '8mimixsn47i',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l491lw6ud7u',
'x-async': false,
'x-index': 1,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': '0y6h0doaa8s',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'cusyvu100n5',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'kp6yhoecxt4',
'x-async': false,
'x-index': 2,
},
},
'x-uid': '6fsjzz00845',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'gbcojp8p120',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '18neqdk2pbg',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '7przcz662jy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'teroosp0elp',
'x-async': true,
'x-index': 1,
},
};

View File

@ -0,0 +1,85 @@
import { createTableBlockUISchema } from '../createTableBlockUISchema';
vi.mock('@formily/shared', () => {
return {
uid: () => 'mocked-uid',
};
});
describe('createTableBLockSchemaV2', () => {
it('should create a default table block schema with minimum options', () => {
const options = { dataSource: 'abc', collectionName: 'users', association: 'users.roles', rowKey: 'rowKey' };
const schema = createTableBlockUISchema(options);
expect(schema).toMatchInlineSnapshot(`
{
"properties": {
"actions": {
"properties": {},
"type": "void",
"x-component": "ActionBar",
"x-component-props": {
"style": {
"marginBottom": "var(--nb-spacing)",
},
},
"x-initializer": "table:configureActions",
},
"mocked-uid": {
"properties": {
"actions": {
"properties": {
"mocked-uid": {
"type": "void",
"x-component": "Space",
"x-component-props": {
"split": "|",
},
"x-decorator": "DndContext",
},
},
"title": "{{ t("Actions") }}",
"type": "void",
"x-action-column": "actions",
"x-component": "TableV2.Column",
"x-decorator": "TableV2.Column.ActionBar",
"x-designer": "TableV2.ActionColumnDesigner",
"x-initializer": "table:configureItemActions",
},
},
"type": "array",
"x-component": "TableV2",
"x-component-props": {
"rowKey": "id",
"rowSelection": {
"type": "checkbox",
},
},
"x-initializer": "table:configureColumns",
"x-use-component-props": "useTableBlockProps",
},
},
"type": "void",
"x-acl-action": "users:list",
"x-component": "CardItem",
"x-decorator": "TableBlockProvider",
"x-decorator-props": {
"action": "list",
"association": "users.roles",
"collection": "users",
"dataSource": "abc",
"dragSort": false,
"params": {
"pageSize": 20,
},
"rowKey": "rowKey",
"showIndex": true,
},
"x-filter-targets": [],
"x-settings": "blockSettings:table",
"x-toolbar": "BlockSchemaToolbar",
"x-use-decorator-props": "useTableBlockDecoratorProps",
}
`);
});
});

View File

@ -0,0 +1,84 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
export const createTableBlockUISchema = (options: {
dataSource: string;
collectionName?: string;
rowKey?: string;
association?: string;
}): ISchema => {
const { collectionName, dataSource, rowKey, association } = options;
if (!dataSource) {
throw new Error('dataSource is required');
}
return {
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': `${collectionName}:list`,
'x-use-decorator-props': 'useTableBlockDecoratorProps',
'x-decorator-props': {
collection: collectionName,
association,
dataSource,
action: 'list',
params: {
pageSize: 20,
},
rowKey,
showIndex: true,
dragSort: false,
},
'x-toolbar': 'BlockSchemaToolbar',
'x-settings': 'blockSettings:table',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
type: 'void',
'x-initializer': 'table:configureActions',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {},
},
[uid()]: {
type: 'array',
'x-initializer': 'table:configureColumns',
'x-component': 'TableV2',
'x-use-component-props': 'useTableBlockProps',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
},
properties: {
actions: {
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'table:configureItemActions',
properties: {
[uid()]: {
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
},
},
},
},
},
},
};
};

View File

@ -0,0 +1,51 @@
import { useFieldSchema } from '@formily/react';
import { useParsedFilter } from '../../../../../block-provider/hooks/useParsedFilter';
import { useMemo } from 'react';
import { useDataBlockSourceId } from '../../../../../block-provider/hooks/useDataBlockSourceId';
export const useTableBlockDecoratorProps = (props) => {
const params = useTableBlockParams(props);
const sourceId = useTableBlockSourceId(props);
return {
params,
sourceId,
};
};
export function useTableBlockParams(props) {
const fieldSchema = useFieldSchema();
const { filter: parsedFilter } = useParsedFilter({
filterOption: props.params?.filter,
});
return useMemo(() => {
const params = props.params || {};
// 1. sort
const { dragSortBy } = fieldSchema?.['x-decorator-props'] || {};
if (props.dragSort && dragSortBy) {
params['sort'] = dragSortBy;
}
// 2. filter
const paramsWithFilter = {
...params,
filter: parsedFilter,
};
return paramsWithFilter;
}, [fieldSchema, parsedFilter, props.dragSort, props.params]);
}
function useTableBlockSourceId(props) {
let sourceId: string | undefined;
// 因为 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,121 @@
import { ArrayField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { useEffect } from 'react';
import { useFilterBlock } from '../../../../../filter-provider/FilterProvider';
import { mergeFilter } from '../../../../../filter-provider/utils';
import { removeNullCondition } from '../../../../../schema-component';
import { findFilterTargets } from '../../../../../block-provider/hooks';
import { useTableBlockContext } from '../../../../../block-provider/TableBlockProvider';
export const useTableBlockProps = () => {
const field = useField<ArrayField>();
const fieldSchema = useFieldSchema();
const ctx = useTableBlockContext();
const globalSort = fieldSchema.parent?.['x-decorator-props']?.['params']?.['sort'];
const { getDataBlocks } = useFilterBlock();
useEffect(() => {
if (!ctx?.service?.loading) {
field.value = [];
field.value = ctx?.service?.data?.data;
field?.setInitialValue(ctx?.service?.data?.data);
field.data = field.data || {};
field.data.selectedRowKeys = ctx?.field?.data?.selectedRowKeys;
field.componentProps.pagination = field.componentProps.pagination || {};
field.componentProps.pagination.pageSize = ctx?.service?.data?.meta?.pageSize;
field.componentProps.pagination.total = ctx?.service?.data?.meta?.count;
field.componentProps.pagination.current = ctx?.service?.data?.meta?.page;
}
}, [ctx?.service?.data, ctx?.service?.loading]); // 这里如果依赖了 ctx?.field?.data?.selectedRowKeys 的话,会导致这个问题:
return {
childrenColumnName: ctx.childrenColumnName,
loading: ctx?.service?.loading,
showIndex: ctx.showIndex,
dragSort: ctx.dragSort && ctx.dragSortBy,
rowKey: ctx.rowKey || 'id',
pagination:
ctx?.params?.paginate !== false
? {
defaultCurrent: ctx?.params?.page || 1,
defaultPageSize: ctx?.params?.pageSize,
}
: false,
onRowSelectionChange(selectedRowKeys) {
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx?.field?.onRowSelect?.(selectedRowKeys);
},
async onRowDragEnd({ from, to }) {
await ctx.resource.move({
sourceId: from[ctx.rowKey || 'id'],
targetId: to[ctx.rowKey || 'id'],
sortField: ctx.dragSort && ctx.dragSortBy,
});
ctx.service.refresh();
},
onChange({ current, pageSize }, filters, sorter) {
const sort = sorter.order ? (sorter.order === `ascend` ? [sorter.field] : [`-${sorter.field}`]) : globalSort;
ctx.service.run({ ...ctx.service.params?.[0], page: current, pageSize, sort });
},
onClickRow(record, setSelectedRow, selectedRow) {
const { targets, uid } = findFilterTargets(fieldSchema);
const dataBlocks = getDataBlocks();
// 如果是之前创建的区块是没有 x-filter-targets 属性的,所以这里需要判断一下避免报错
if (!targets || !targets.some((target) => dataBlocks.some((dataBlock) => dataBlock.uid === target.uid))) {
// 当用户已经点击过某一行,如果此时再把相连接的区块给删除的话,行的高亮状态就会一直保留。
// 这里暂时没有什么比较好的方法,只是在用户再次点击的时候,把高亮状态给清除掉。
setSelectedRow((prev) => (prev.length ? [] : prev));
return;
}
const value = [record[ctx.rowKey]];
dataBlocks.forEach((block) => {
const target = targets.find((target) => target.uid === block.uid);
if (!target) return;
const param = block.service.params?.[0] || {};
// 保留原有的 filter
const storedFilter = block.service.params?.[1]?.filters || {};
if (selectedRow.includes(record[ctx.rowKey])) {
if (block.dataLoadingMode === 'manual') {
return block.clearData();
}
delete storedFilter[uid];
} else {
storedFilter[uid] = {
$and: [
{
[target.field || ctx.rowKey]: {
[target.field ? '$in' : '$eq']: value,
},
},
],
};
}
const mergedFilter = mergeFilter([
...Object.values(storedFilter).map((filter) => removeNullCondition(filter)),
block.defaultFilter,
]);
return block.doFilter(
{
...param,
page: 1,
filter: mergedFilter,
},
{ filters: storedFilter },
);
});
// 更新表格的选中状态
setSelectedRow((prev) => (prev?.includes(record[ctx.rowKey]) ? [] : [...value]));
},
onExpand(expanded, record) {
ctx?.field.onExpandClick?.(expanded, record);
},
};
};

View File

@ -5,3 +5,5 @@ export * from './TableColumnSchemaToolbar';
export * from './tableBlockSettings'; export * from './tableBlockSettings';
export * from './tableColumnSettings'; export * from './tableColumnSettings';
export * from './TableColumnInitializers'; export * from './TableColumnInitializers';
export * from './createTableBlockUISchema';
export * from './hooks/useTableBlockDecoratorProps';

View File

@ -359,7 +359,8 @@ export const tableBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Table', componentName: 'Table',
collectionName: name, collectionName: name,

View File

@ -22,7 +22,8 @@ export const filterCollapseBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FilterCollapse', componentName: 'FilterCollapse',

View File

@ -23,7 +23,8 @@ export const filterFormBlockSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FilterFormItem', componentName: 'FilterFormItem',
collectionName: name, collectionName: name,

View File

@ -0,0 +1,26 @@
import { useField, useFieldSchema, useForm } from '@formily/react';
import { SchemaSettings } from '../../../../application/schema-settings/SchemaSettings';
import { SchemaSettingsNumberFormat } from '../../../../schema-settings/SchemaSettingsNumberFormat';
import { useColumnSchema } from '../../../../schema-component/antd/table-v2/Table.Column.Decorator';
import { useIsFieldReadPretty } from '../../../../schema-component/antd/form-item/FormItem.Settings';
export const inputNumberComponentFieldSettings = new SchemaSettings({
name: 'fieldSettings:component:InputNumber',
items: [
{
name: 'displayFormat',
Component: SchemaSettingsNumberFormat as any,
useComponentProps() {
const schema = useFieldSchema();
const { fieldSchema: tableColumnSchema } = useColumnSchema();
const fieldSchema = tableColumnSchema || schema;
return {
fieldSchema,
};
},
useVisible() {
const isFieldReadPretty = useIsFieldReadPretty();
return isFieldReadPretty;
},
},
],
});

View File

@ -92,7 +92,7 @@ const LocalPlugins = () => {
const keyWordsfilterList = useMemo(() => { const keyWordsfilterList = useMemo(() => {
const list = keyWordlists.map((i) => { const list = keyWordlists.map((i) => {
if (i === 'Others') { if (i === 'Others') {
const result = data?.data.filter((v) => !v.keywords); const result = data?.data.filter((v) => !v.keywords || !v.keywords.every((k) => keyWordlists.includes(k)));
return { return {
key: i, key: i,
list: result, list: result,

View File

@ -11,7 +11,11 @@ import { usePlugin } from '../../../application/hooks';
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings'; import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar'; import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useFormBlockContext } from '../../../block-provider'; import { useFormBlockContext } from '../../../block-provider';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; import {
joinCollectionName,
useCollectionManager_deprecated,
useCollection_deprecated,
} from '../../../collection-manager';
import { FlagProvider } from '../../../flag-provider'; import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings'; import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
@ -28,6 +32,7 @@ import {
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useLinkageAction } from './hooks'; import { useLinkageAction } from './hooks';
import { requestSettingsSchema } from './utils'; import { requestSettingsSchema } from './utils';
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
const MenuGroup = (props) => { const MenuGroup = (props) => {
return props.children; return props.children;
@ -327,7 +332,8 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
const { setValuesIn } = useForm(); const { setValuesIn } = useForm();
const baseCollection = useCollection_deprecated(); const baseCollection = useCollection_deprecated();
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
const [workflowCollection, setWorkflowCollection] = useState(baseCollection.name); const dataSourceKey = useDataSourceKey();
const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name));
const compile = useCompile(); const compile = useCompile();
const workflowPlugin = usePlugin('workflow') as any; const workflowPlugin = usePlugin('workflow') as any;
@ -351,11 +357,11 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
const path = paths[i]; const path = paths[i];
const associationField = collection.fields.find((f) => f.name === path); const associationField = collection.fields.find((f) => f.name === path);
if (associationField) { if (associationField) {
collection = getCollection(associationField.target); collection = getCollection(associationField.target, dataSourceKey);
} }
} }
} }
setWorkflowCollection(collection.name); setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name));
setValuesIn(`group[${index}].workflowKey`, null); setValuesIn(`group[${index}].workflowKey`, null);
}); });
}); });
@ -375,38 +381,40 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
); );
return ( return (
<RemoteSelect <DataSourceProvider dataSource="main">
manual={false} <RemoteSelect
placeholder={t('Select workflow', { ns: 'workflow' })} manual={false}
fieldNames={{ placeholder={t('Select workflow', { ns: 'workflow' })}
label: 'title', fieldNames={{
value: 'key', label: 'title',
}} value: 'key',
service={{ }}
resource: 'workflows', service={{
action: 'list', resource: 'workflows',
params: { action: 'list',
filter: { params: {
type: workflowTypes, filter: {
enabled: true, type: workflowTypes,
'config.collection': workflowCollection, enabled: true,
'config.collection': workflowCollection,
},
}, },
}, }}
}} optionFilter={optionFilter}
optionFilter={optionFilter} optionRender={({ label, data }) => {
optionRender={({ label, data }) => { const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type);
const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type); return typeOption ? (
return typeOption ? ( <Flex justify="space-between">
<Flex justify="space-between"> <span>{label}</span>
<span>{label}</span> <Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag> </Flex>
</Flex> ) : (
) : ( label
label );
); }}
}} {...props}
{...props} />
/> </DataSourceProvider>
); );
} }
@ -414,7 +422,7 @@ export function WorkflowConfig() {
const { dn } = useDesignable(); const { dn } = useDesignable();
const { t } = useTranslation(); const { t } = useTranslation();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { name: collection } = useCollection_deprecated(); const collection = useCollection_deprecated();
// TODO(refactor): should refactor for getting certain action type, better from 'x-action'. // TODO(refactor): should refactor for getting certain action type, better from 'x-action'.
const formBlock = useFormBlockContext(); const formBlock = useFormBlockContext();
const actionType = formBlock?.type || fieldSchema['x-action']; const actionType = formBlock?.type || fieldSchema['x-action'];
@ -483,7 +491,9 @@ export function WorkflowConfig() {
'x-component-props': { 'x-component-props': {
placeholder: t('Select context', { ns: 'workflow' }), placeholder: t('Select context', { ns: 'workflow' }),
popupMatchSelectWidth: false, popupMatchSelectWidth: false,
collection, collection: `${
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
}${collection.name}`,
filter: '{{ fieldFilter }}', filter: '{{ fieldFilter }}',
rootOption: { rootOption: {
label: t('Full form data', { ns: 'workflow' }), label: t('Full form data', { ns: 'workflow' }),

View File

@ -3,7 +3,12 @@ import { Tag, TreeSelect } from 'antd';
import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect'; import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated, useCompile } from '../../..'; import {
CollectionFieldOptions_deprecated,
parseCollectionName,
useCollectionManager_deprecated,
useCompile,
} from '../../..';
export type AppendsTreeSelectProps = { export type AppendsTreeSelectProps = {
value: string[] | string; value: string[] | string;
@ -27,7 +32,7 @@ function usePropsCollection({ collection }) {
type CallScope = { type CallScope = {
compile?(value: string): string; compile?(value: string): string;
getCollectionFields?(name: any): CollectionFieldOptions_deprecated[]; getCollectionFields?(name: any, dataSource?: string): CollectionFieldOptions_deprecated[];
filter(field): boolean; filter(field): boolean;
}; };
@ -52,7 +57,8 @@ function trueFilter(field) {
} }
function getCollectionFieldOptions(this: CallScope, collection, parentNode?): TreeOptionType[] { function getCollectionFieldOptions(this: CallScope, collection, parentNode?): TreeOptionType[] {
const fields = this.getCollectionFields(collection).filter(isAssociation); const [dataSourceName, collectionName] = parseCollectionName(collection);
const fields = this.getCollectionFields(collectionName, dataSourceName).filter(isAssociation);
const boundLoadChildren = loadChildren.bind(this); const boundLoadChildren = loadChildren.bind(this);
return fields.filter(this.filter).map((field) => { return fields.filter(this.filter).map((field) => {
const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name; const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name;
@ -84,11 +90,12 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
loadData: propsLoadData, loadData: propsLoadData,
...restProps ...restProps
} = props; } = props;
const { getCollectionFields } = useCollectionManager_deprecated();
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation(); const { t } = useTranslation();
const [optionsMap, setOptionsMap] = useState({}); const [optionsMap, setOptionsMap] = useState({});
const baseCollection = useCollection({ collection }); const collectionString = useCollection({ collection });
const [dataSourceName, collectionName] = parseCollectionName(collectionString);
const { getCollectionFields } = useCollectionManager_deprecated(dataSourceName);
const treeData = Object.values(optionsMap); const treeData = Object.values(optionsMap);
const value: string | DefaultOptionType[] = useMemo(() => { const value: string | DefaultOptionType[] = useMemo(() => {
if (props.multiple) { if (props.multiple) {
@ -111,7 +118,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
}, },
[propsLoadData], [propsLoadData],
); );
// NOTE:
useEffect(() => { useEffect(() => {
const parentNode = rootOption const parentNode = rootOption
? { ? {
@ -123,17 +130,19 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
isLeaf: false, isLeaf: false,
} }
: null; : null;
const treeData = const tData =
propsLoadData === null propsLoadData === null
? [] ? []
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, baseCollection, parentNode); : getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode);
const map = treeData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
if (parentNode) { if (parentNode) {
map[parentNode.value] = parentNode; map[parentNode.value] = parentNode;
} }
setOptionsMap(map); setOptionsMap(map);
}, [collection, baseCollection, rootOption, filter, propsLoadData]); }, [collectionString, rootOption, filter, propsLoadData]);
// NOTE: preload options in value
useEffect(() => { useEffect(() => {
const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[]; const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[];
if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) { if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) {

View File

@ -18,7 +18,8 @@ export const AssociationFilterBlockDesigner = () => {
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { t } = useTranslation(); const { t } = useTranslation();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return ( return (
<GeneralSchemaDesigner template={template} title={title || name}> <GeneralSchemaDesigner template={template} title={title || name}>

View File

@ -1,7 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { Button } from 'antd'; import { Button } from 'antd';
import React from 'react'; import React, { forwardRef, createRef } from 'react';
import { composeRef } from 'rc-util/lib/ref';
import { useCompile } from '../../hooks'; import { useCompile } from '../../hooks';
import { useTableBlockContext, useTableSelectorContext } from '../../../block-provider'; import { useTableBlockContext, useTableSelectorContext } from '../../../block-provider';
import { Icon } from '../../../icon'; import { Icon } from '../../../icon';
@ -46,7 +47,7 @@ const actionDesignerCss = css`
} }
`; `;
export const ExpandAction = (props) => { const InternalExpandAction = (props, ref) => {
const schema = useFieldSchema(); const schema = useFieldSchema();
const ctxSelector = useTableSelectorContext(); const ctxSelector = useTableSelectorContext();
const ctxBlock = useTableBlockContext(); const ctxBlock = useTableBlockContext();
@ -54,8 +55,11 @@ export const ExpandAction = (props) => {
const ctx = isTableSelector ? ctxSelector : ctxBlock; const ctx = isTableSelector ? ctxSelector : ctxBlock;
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {}; const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {};
const compile = useCompile(); const compile = useCompile();
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
const buttonRef = composeRef(ref, internalRef);
return ( return (
<div className={actionDesignerCss}> //@ts-ignore
<div className={actionDesignerCss} ref={buttonRef as React.Ref<HTMLButtonElement>}>
{ctx?.params['tree'] && ( {ctx?.params['tree'] && (
<Button <Button
onClick={() => { onClick={() => {
@ -63,6 +67,7 @@ export const ExpandAction = (props) => {
}} }}
icon={<Icon type={ctx?.expandFlag ? iconCollapse : iconExpand} />} icon={<Icon type={ctx?.expandFlag ? iconCollapse : iconExpand} />}
type={props.type} type={props.type}
style={props?.style}
> >
{props.children?.[1]} {props.children?.[1]}
<span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? compile(titleCollapse) : compile(titleExpand)}</span> <span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? compile(titleCollapse) : compile(titleExpand)}</span>
@ -71,3 +76,5 @@ export const ExpandAction = (props) => {
</div> </div>
); );
}; };
export const ExpandAction = forwardRef<HTMLButtonElement | HTMLAnchorElement, any>(InternalExpandAction);

View File

@ -62,7 +62,8 @@ export const formSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'FormItem', componentName: 'FormItem',
collectionName: name, collectionName: name,
@ -103,7 +104,8 @@ export const readPrettyFormSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
insertAdjacentPosition: 'beforeEnd', insertAdjacentPosition: 'beforeEnd',
componentName: 'ReadPrettyFormItem', componentName: 'ReadPrettyFormItem',
@ -294,7 +296,8 @@ export const formDetailsSettings = new SchemaSettings({
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
return { return {
componentName: 'Details', componentName: 'Details',
collectionName: name, collectionName: name,

View File

@ -39,7 +39,8 @@ export const GridCardDesigner = () => {
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord(); const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
const columnCount = field.decoratorProps.columnCount || defaultColumnCount; const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
const columnCountSchema = useMemo(() => { const columnCountSchema = useMemo(() => {

View File

@ -2,21 +2,125 @@ import { isValid } from '@formily/shared';
import { toFixedByStep } from '@nocobase/utils/client'; import { toFixedByStep } from '@nocobase/utils/client';
import type { InputProps } from 'antd/es/input'; import type { InputProps } from 'antd/es/input';
import type { InputNumberProps } from 'antd/es/input-number'; import type { InputNumberProps } from 'antd/es/input-number';
import React from 'react'; import { format } from 'd3-format';
import * as math from 'mathjs';
import React, { useMemo } from 'react';
function countDecimalPlaces(value) {
const number = Number(value);
if (!Number.isFinite(number)) return 0;
const decimalPart = String(number).split('.')[1];
return decimalPart ? decimalPart.length : 0;
}
const separators = {
'0,0.00': { thousands: ',', decimal: '.' },
'0.0,00': { thousands: '.', decimal: ',' },
'0 0,00': { thousands: ' ', decimal: '.' },
'0.00': { thousands: '', decimal: '.' }, // 没有千位分隔符
};
//分隔符换算
export function formatNumberWithSeparator(number, format = '0.00', step = 1) {
let formattedNumber = '';
if (separators[format]) {
const { thousands, decimal } = separators[format];
formattedNumber = number
.toLocaleString('en-US', {
style: 'decimal',
minimumFractionDigits: step,
maximumFractionDigits: step,
})
.replace(/,/g, 'comma_placeholder')
.replace(/\./g, 'dot_placeholder')
.replace(/comma_placeholder/g, thousands)
.replace(/dot_placeholder/g, decimal);
} else {
formattedNumber = number.toString();
}
return formattedNumber;
}
//单位换算
export function formatUnitConversion(value, operator = '*', multiplier) {
if (!multiplier) {
return value;
}
let result;
if (operator === '*') {
result = value * multiplier;
} else if (operator === '/') {
if (multiplier !== 0) {
result = value / multiplier;
} else {
console.error('Error: Division by zero.');
return null;
}
} else {
console.error("Error: Invalid operator. Use '*' for multiplication or '/' for division.");
return null;
}
return math.round(result, 9);
}
//科学计数法显示
export function scientificNotation(number, decimalPlaces, separator = '.') {
const formatter = format(`.${decimalPlaces}e`);
const formattedNumber = formatter(number).replace('.', separator);
// 匹配科学计数法中的指数部分,判断正负情况
const result = formattedNumber.replace(/e([+-]?\d+)/, (match, exponent) => {
if (exponent.startsWith('+')) {
// 正数指数,不显示符号
return `×10<sup>${exponent.slice(1)}</sup>`;
} else {
// 负数指数,显示 "-" 符号
return `×10<sup>-${exponent.slice(1)}</sup>`;
}
});
return result;
}
export function formatNumber(props) {
const { step, formatStyle, value, unitConversion, unitConversionType, separator = '0.00' } = props;
if (!isValid(value)) {
return null;
}
//单位换算
const unitData = formatUnitConversion(value, unitConversionType, unitConversion);
//精度换算
const preciationData = toFixedByStep(unitData, step);
let result;
//分隔符换算
result = formatNumberWithSeparator(Number(preciationData), separator, countDecimalPlaces(step));
if (formatStyle === 'scientifix') {
//科学计数显示
result = scientificNotation(Number(unitData), countDecimalPlaces(step), separators?.[separator]?.['decimal']);
}
return result;
}
export const ReadPretty: React.FC<InputProps & InputNumberProps> = (props: any) => { export const ReadPretty: React.FC<InputProps & InputNumberProps> = (props: any) => {
const { step, value, addonBefore, addonAfter } = props; const { step, formatStyle, value, addonBefore, addonAfter, unitConversion, unitConversionType, separator } = props;
if (!isValid(props.value)) {
return null; const result = useMemo(() => {
} return formatNumber({ step, formatStyle, value, unitConversion, unitConversionType, separator });
const result = toFixedByStep(value, step); }, [step, formatStyle, value, unitConversion, unitConversionType, separator]);
if (isNaN(result)) {
if (!isValid(result)) {
return null; return null;
} }
return ( return (
<div className={'nb-read-pretty-input-number'}> <div className={'nb-read-pretty-input-number'}>
{addonBefore} {addonBefore}
{result} <span dangerouslySetInnerHTML={{ __html: result }} />
{addonAfter} {addonAfter}
</div> </div>
); );

View File

@ -1,5 +1,6 @@
import { fireEvent, render, screen } from '@nocobase/test/client'; import { fireEvent, render, screen } from '@nocobase/test/client';
import React from 'react'; import React from 'react';
import { formatNumberWithSeparator, formatUnitConversion, scientificNotation } from '../ReadPretty';
import App2 from '../demos/addonBefore&addonAfter'; import App2 from '../demos/addonBefore&addonAfter';
import App3 from '../demos/highPrecisionDecimals'; import App3 from '../demos/highPrecisionDecimals';
import App1 from '../demos/inputNumber'; import App1 from '../demos/inputNumber';
@ -43,7 +44,7 @@ describe('InputNumber: addonBefore/addonAfter', () => {
fireEvent.change(input, { target: { value: 1 } }); fireEvent.change(input, { target: { value: 1 } });
expect(input.value).toBe('1'); expect(input.value).toBe('1');
// @ts-ignore // @ts-ignore
expect(screen.getByText(1万元')).toBeInTheDocument(); expect(screen.getByText(')).toBeInTheDocument();
// empty value // empty value
fireEvent.change(input, { target: { value: '' } }); fireEvent.change(input, { target: { value: '' } });
@ -67,7 +68,7 @@ describe('InputNumber: High precision decimals', () => {
fireEvent.change(input, { target: { value: 1 } }); fireEvent.change(input, { target: { value: 1 } });
expect(input.value).toBe('1.00'); expect(input.value).toBe('1.00');
// @ts-ignore // @ts-ignore
expect(screen.getByText('1.00%')).toBeInTheDocument(); expect(screen.getByText('1.00')).toBeInTheDocument();
// empty value // empty value
fireEvent.change(input, { target: { value: '' } }); fireEvent.change(input, { target: { value: '' } });
@ -75,3 +76,48 @@ describe('InputNumber: High precision decimals', () => {
expect(screen.queryByText('NaN')).toBeNull(); expect(screen.queryByText('NaN')).toBeNull();
}); });
}); });
describe('ReadPretty:formatNumberWithSeparator', () => {
// Test case 1: Format a number with default format '0,0.00'
test('Format number with default separator', () => {
const formatted = formatNumberWithSeparator(1234567.89);
expect(formatted).toBe('1234567.9');
});
// Test case 2: Format a number with custom format '0.00'
test('Format number with custom separator', () => {
const formatted = formatNumberWithSeparator(1234567.89, '0,0.00', 1);
expect(formatted).toBe('1,234,567.9');
});
});
describe('ReadPretty:formatUnitConversion', () => {
// Test case 1: Multiply a value by 2
test('Multiply value by 2', () => {
const result = formatUnitConversion(10, '*', 2);
expect(result).toBe(20);
});
// Test case 2: Divide a value by 0 (error case)
test('Divide value by zero', () => {
const result = formatUnitConversion(10, '/', 0);
expect(result).toBe(10);
});
test('0.1*0.2', () => {
const result = formatUnitConversion(0.1, '*', 0.2);
expect(result).toBe(0.02);
});
});
describe('ReadPretty:scientificNotation', () => {
// Test case 1: Format a number into scientific notation with 2 decimal places
test('Format number into scientific notation', () => {
const formatted = scientificNotation(1234567.89, 2);
expect(formatted).toBe('1.23×10<sup>6</sup>');
});
// Test case 2: Format a number into scientific notation with custom separator '.'
test('Format number into scientific notation with custom separator', () => {
const formatted = scientificNotation(1234567.89, 2, '.');
expect(formatted).toBe('1.23×10<sup>6</sup>');
});
});

View File

@ -35,7 +35,8 @@ export const ListDesigner = () => {
const sortFields = useSortFields(name); const sortFields = useSortFields(name);
const record = useRecord(); const record = useRecord();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
const sort = defaultSort?.map((item: string) => { const sort = defaultSort?.map((item: string) => {
return item.startsWith('-') return item.startsWith('-')
? { ? {

View File

@ -25,6 +25,7 @@ import {
useTableBlockContext, useTableBlockContext,
useTableSelectorContext, useTableSelectorContext,
} from '../../../'; } from '../../../';
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useACLFieldWhitelist } from '../../../acl/ACLProvider'; import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { useToken } from '../__builtins__'; import { useToken } from '../__builtins__';
import { SubFormProvider } from '../association-field/hooks'; import { SubFormProvider } from '../association-field/hooks';
@ -242,195 +243,200 @@ const usePaginationProps = (pagination1, pagination2) => {
return result.total <= result.pageSize ? false : result; return result.total <= result.pageSize ? false : result;
}; };
export const Table: any = observer( export const Table: any = withDynamicSchemaProps(
(props: { observer(
useProps?: () => any; (props: {
onChange?: (pagination, filters, sorter, extra) => void; useProps?: () => any;
onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void; onChange?: (pagination, filters, sorter, extra) => void;
onRowDragEnd?: (e: { from: any; to: any }) => void; onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void;
onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void; onRowDragEnd?: (e: { from: any; to: any }) => void;
pagination?: any; onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void;
showIndex?: boolean; pagination?: any;
dragSort?: boolean; showIndex?: boolean;
rowKey?: string | ((record: any) => string); dragSort?: boolean;
rowSelection?: any; rowKey?: string | ((record: any) => string);
required?: boolean; rowSelection?: any;
onExpand?: (flag: boolean, record: any) => void; required?: boolean;
isSubTable?: boolean; onExpand?: (flag: boolean, record: any) => void;
}) => { isSubTable?: boolean;
const { token } = useToken(); }) => {
const { pagination: pagination1, useProps, onChange, ...others1 } = props; const { token } = useToken();
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {}; const { pagination: pagination1, useProps, ...others1 } = props;
const {
dragSort = false,
showIndex = true,
onRowSelectionChange,
onChange: onTableChange,
rowSelection,
rowKey,
required,
onExpand,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const collection = useCollection_deprecated();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag, allIncludesChildren } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => {}));
const paginationProps = usePaginationProps(pagination1, pagination2);
const [expandedKeys, setExpandesKeys] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || [];
const isRowSelect = rowSelection?.type !== 'none';
const defaultRowKeyMap = useRef(new Map());
let onRow = null,
highlightRow = '';
if (onClickRow) { // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
onRow = (record) => { const { pagination: pagination2, ...others2 } = useProps?.() || {};
return {
onClick: (e) => { const {
if (isPortalInBody(e.target)) { dragSort = false,
return; showIndex = true,
} onRowSelectionChange,
onClickRow(record, setSelectedRow, selectedRow); onChange: onTableChange,
}, rowSelection,
rowKey,
required,
onExpand,
onClickRow,
...others
} = { ...others1, ...others2 } as any;
const field = useArrayField(others);
const columns = useTableColumns(others);
const schema = useFieldSchema();
const collection = useCollection_deprecated();
const isTableSelector = schema?.parent?.['x-decorator'] === 'TableSelectorProvider';
const ctx = isTableSelector ? useTableSelectorContext() : useTableBlockContext();
const { expandFlag, allIncludesChildren } = ctx;
const onRowDragEnd = useMemoizedFn(others.onRowDragEnd || (() => { }));
const paginationProps = usePaginationProps(pagination1, pagination2);
const [expandedKeys, setExpandesKeys] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState<any[]>(field?.data?.selectedRowKeys || []);
const [selectedRow, setSelectedRow] = useState([]);
const dataSource = field?.value?.slice?.()?.filter?.(Boolean) || [];
const isRowSelect = rowSelection?.type !== 'none';
const defaultRowKeyMap = useRef(new Map());
let onRow = null,
highlightRow = '';
if (onClickRow) {
onRow = (record) => {
return {
onClick: (e) => {
if (isPortalInBody(e.target)) {
return;
}
onClickRow(record, setSelectedRow, selectedRow);
},
};
}; };
}; highlightRow = css`
highlightRow = css` & > td {
& > td { background-color: ${token.controlItemBgActiveHover} !important;
background-color: ${token.controlItemBgActiveHover} !important; }
} &:hover > td {
&:hover > td { background-color: ${token.controlItemBgActiveHover} !important;
background-color: ${token.controlItemBgActiveHover} !important; }
} `;
`;
}
useEffect(() => {
if (expandFlag) {
setExpandesKeys(allIncludesChildren);
} else {
setExpandesKeys([]);
} }
}, [expandFlag, allIncludesChildren]);
const components = useMemo(() => { useEffect(() => {
return { if (expandFlag) {
header: { setExpandesKeys(allIncludesChildren);
wrapper: (props) => { } else {
return ( setExpandesKeys([]);
<DndContext> }
<thead {...props} /> }, [expandFlag, allIncludesChildren]);
</DndContext>
); const components = useMemo(() => {
return {
header: {
wrapper: (props) => {
return (
<DndContext>
<thead {...props} />
</DndContext>
);
},
cell: (props) => {
return (
<th
{...props}
className={cls(
props.className,
css`
max-width: 300px;
white-space: nowrap;
&:hover .general-schema-designer {
display: block;
}
`,
)}
/>
);
},
}, },
cell: (props) => { body: {
return ( wrapper: (props) => {
<th return (
<DndContext
onDragEnd={(e) => {
if (!e.active || !e.over) {
console.warn('move cancel');
return;
}
const fromIndex = e.active?.data.current?.sortable?.index;
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex] || e.active;
const to = field.value[toIndex] || e.over;
void field.move(fromIndex, toIndex);
onRowDragEnd({ from, to });
}}
>
<tbody {...props} />
</DndContext>
);
},
row: (props) => {
return <SortableRow {...props}></SortableRow>;
},
cell: (props) => (
<td
{...props} {...props}
className={cls( className={classNames(
props.className, props.className,
css` css`
max-width: 300px; max-width: 300px;
white-space: nowrap; white-space: nowrap;
&:hover .general-schema-designer { .nb-read-pretty-input-number {
display: block; text-align: right;
}
.ant-color-picker-trigger {
position: absolute;
top: 50%;
transform: translateY(-50%);
} }
`, `,
)} )}
/> />
); ),
}, },
}, };
body: { }, [field, onRowDragEnd, dragSort]);
wrapper: (props) => {
return ( /**
<DndContext * key key
onDragEnd={(e) => { * 1. rowKey key record.key
if (!e.active || !e.over) { * 2. key record
console.warn('move cancel'); * 3. key
return; *
} * record
const fromIndex = e.active?.data.current?.sortable?.index; *
const toIndex = e.over?.data.current?.sortable?.index; * @param record
const from = field.value[fromIndex] || e.active; * @returns
const to = field.value[toIndex] || e.over; */
void field.move(fromIndex, toIndex); const defaultRowKey = (record: any) => {
onRowDragEnd({ from, to }); if (record.key) {
}} return record.key;
> }
<tbody {...props} />
</DndContext> if (defaultRowKeyMap.current.has(record)) {
); return defaultRowKeyMap.current.get(record);
}, }
row: (props) => {
return <SortableRow {...props}></SortableRow>; const key = uid();
}, defaultRowKeyMap.current.set(record, key);
cell: (props) => ( return key;
<td
{...props}
className={classNames(
props.className,
css`
max-width: 300px;
white-space: nowrap;
.nb-read-pretty-input-number {
text-align: right;
}
.ant-color-picker-trigger {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
`,
)}
/>
),
},
}; };
}, [field, onRowDragEnd, dragSort]);
/** const getRowKey = (record: any) => {
* key key if (typeof rowKey === 'string') {
* 1. rowKey key record.key return record[rowKey]?.toString();
* 2. key record } else {
* 3. key return (rowKey ?? defaultRowKey)(record)?.toString();
* }
* record };
*
* @param record
* @returns
*/
const defaultRowKey = (record: any) => {
if (record.key) {
return record.key;
}
if (defaultRowKeyMap.current.has(record)) { const restProps = {
return defaultRowKeyMap.current.get(record); rowSelection: rowSelection
} ? {
const key = uid();
defaultRowKeyMap.current.set(record, key);
return key;
};
const getRowKey = (record: any) => {
if (typeof rowKey === 'string') {
return record[rowKey]?.toString();
} else {
return (rowKey ?? defaultRowKey)(record)?.toString();
}
};
const restProps = {
rowSelection: rowSelection
? {
type: 'checkbox', type: 'checkbox',
selectedRowKeys: selectedRowKeys, selectedRowKeys: selectedRowKeys,
onChange(selectedRowKeys: any[], selectedRows: any[]) { onChange(selectedRowKeys: any[], selectedRows: any[]) {
@ -465,32 +471,32 @@ export const Table: any = observer(
className={classNames( className={classNames(
checked ? 'checked' : null, checked ? 'checked' : null,
css` css`
position: relative; position: relative;
display: flex; display: flex;
float: left; float: left;
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-evenly;
padding-right: 8px; padding-right: 8px;
.nb-table-index {
opacity: 0;
}
&:not(.checked) {
.nb-table-index { .nb-table-index {
opacity: 1; opacity: 0;
} }
} &:not(.checked) {
`, .nb-table-index {
opacity: 1;
}
}
`,
{ {
[css` [css`
&:hover { &:hover {
.nb-table-index { .nb-table-index {
opacity: 0; opacity: 0;
}
.nb-origin-node {
display: block;
}
} }
.nb-origin-node { `]: isRowSelect,
display: block;
}
}
`]: isRowSelect,
}, },
)} )}
> >
@ -498,11 +504,11 @@ export const Table: any = observer(
className={classNames( className={classNames(
checked ? 'checked' : null, checked ? 'checked' : null,
css` css`
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-evenly; justify-content: space-evenly;
`, `,
)} )}
> >
{dragSort && <SortHandle id={getRowKey(record)} />} {dragSort && <SortHandle id={getRowKey(record)} />}
@ -514,13 +520,13 @@ export const Table: any = observer(
'nb-origin-node', 'nb-origin-node',
checked ? 'checked' : null, checked ? 'checked' : null,
css` css`
position: absolute; position: absolute;
right: 50%; right: 50%;
transform: translateX(50%); transform: translateX(50%);
&:not(.checked) { &:not(.checked) {
display: none; display: none;
} }
`, `,
)} )}
> >
{originNode} {originNode}
@ -531,101 +537,101 @@ export const Table: any = observer(
}, },
...rowSelection, ...rowSelection,
} }
: undefined, : undefined,
}; };
const SortableWrapper = useCallback<React.FC>( const SortableWrapper = useCallback<React.FC>(
({ children }) => { ({ children }) => {
return dragSort return dragSort
? React.createElement<Omit<SortableContextProps, 'children'>>( ? React.createElement<Omit<SortableContextProps, 'children'>>(
SortableContext, SortableContext,
{ {
items: field.value?.map?.(getRowKey) || [], items: field.value?.map?.(getRowKey) || [],
}, },
children, children,
) )
: React.createElement(React.Fragment, {}, children); : React.createElement(React.Fragment, {}, children);
}, },
[field, dragSort], [field, dragSort],
); );
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock; const fixedBlock = fieldSchema?.parent?.['x-decorator-props']?.fixedBlock;
const { height: tableHeight, tableSizeRefCallback } = useTableSize(); const { height: tableHeight, tableSizeRefCallback } = useTableSize();
const scroll = useMemo(() => { const scroll = useMemo(() => {
return fixedBlock return fixedBlock
? { ? {
x: 'max-content', x: 'max-content',
y: tableHeight, y: tableHeight,
} }
: { : {
x: 'max-content', x: 'max-content',
}; };
}, [fixedBlock, tableHeight]); }, [fixedBlock, tableHeight]);
return ( return (
<div <div
className={css` className={css`
height: 100%;
overflow: hidden;
.ant-table-wrapper {
height: 100%; height: 100%;
.ant-spin-nested-loading { overflow: hidden;
.ant-table-thead {
.ant-table-cell {
text-align: center;
}
.ant-table-wrapper {
height: 100%; height: 100%;
.ant-spin-container { .ant-spin-nested-loading {
height: 100%; height: 100%;
display: flex; .ant-spin-container {
flex-direction: column; height: 100%;
.ant-table-thead { display: flex;
.ant-table-cell { flex-direction: column;
text-align: center;
}
} }
} }
} }
} .ant-table {
.ant-table { overflow-x: auto;
overflow-x: auto; overflow-y: hidden;
overflow-y: hidden; }
} `}
`} >
> <SortableWrapper>
<SortableWrapper> <AntdTable
<AntdTable ref={tableSizeRefCallback}
ref={tableSizeRefCallback} rowKey={rowKey ?? defaultRowKey}
rowKey={rowKey ?? defaultRowKey} dataSource={dataSource}
dataSource={dataSource} tableLayout="auto"
tableLayout="auto" {...others}
{...others} {...restProps}
{...restProps} pagination={paginationProps}
pagination={paginationProps} components={components}
components={components} onChange={(pagination, filters, sorter, extra) => {
onChange={(pagination, filters, sorter, extra) => { onTableChange?.(pagination, filters, sorter, extra);
onTableChange?.(pagination, filters, sorter, extra); }}
}} onRow={onRow}
onRow={onRow} rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')}
rowClassName={(record) => (selectedRow.includes(record[rowKey]) ? highlightRow : '')} scroll={scroll}
scroll={scroll} columns={columns}
columns={columns} expandable={{
expandable={{ onExpand: (flag, record) => {
onExpand: (flag, record) => { const newKeys = flag
const newKeys = flag ? [...expandedKeys, record[collection.getPrimaryKey()]]
? [...expandedKeys, record[collection.getPrimaryKey()]] : expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i);
: expandedKeys.filter((i) => record[collection.getPrimaryKey()] !== i); setExpandesKeys(newKeys);
setExpandesKeys(newKeys); onExpand?.(flag, record);
onExpand?.(flag, record); },
}, expandedRowKeys: expandedKeys,
expandedRowKeys: expandedKeys, }}
}} />
/> </SortableWrapper>
</SortableWrapper> {field.errors.length > 0 && (
{field.errors.length > 0 && ( <div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active">
<div className="ant-formily-item-error-help ant-formily-item-help ant-formily-item-help-enter ant-formily-item-help-enter-active"> {field.errors.map((error) => {
{field.errors.map((error) => { return error.messages.map((message) => <div key={message}>{message}</div>);
return error.messages.map((message) => <div key={message}>{message}</div>); })}
})} </div>
</div> )}
)} </div>
</div> );
); },
}, ),
{ displayName: 'Table' }, { displayName: 'Table' },
); );

View File

@ -78,7 +78,8 @@ export const TableBlockDesigner = () => {
const { dn } = useDesignable(); const { dn } = useDesignable();
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || []; const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource; const defaultResource =
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate; const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate;
const sort = defaultSort?.map((item: string) => { const sort = defaultSort?.map((item: string) => {
return item?.startsWith('-') return item?.startsWith('-')

View File

@ -2,7 +2,8 @@ import { DownOutlined } from '@ant-design/icons';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { Button, Dropdown, MenuProps } from 'antd'; import { Button, Dropdown, MenuProps } from 'antd';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState, forwardRef, createRef } from 'react';
import { composeRef } from 'rc-util/lib/ref';
import { useDesignable } from '../../'; import { useDesignable } from '../../';
import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider'; import { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
import { import {
@ -96,55 +97,55 @@ function useAclCheckFn() {
return actionAclCheck; return actionAclCheck;
} }
export const CreateRecordAction = observer( const InternalCreateRecordAction = (props: any, ref) => {
(props: any) => { const [visible, setVisible] = useState(false);
const [visible, setVisible] = useState(false); const collection = useCollection_deprecated();
const collection = useCollection_deprecated(); const fieldSchema = useFieldSchema();
const fieldSchema = useFieldSchema(); const field: any = useField();
const field: any = useField(); const [currentCollection, setCurrentCollection] = useState(collection.name);
const [currentCollection, setCurrentCollection] = useState(collection.name); const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource);
const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource); const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; const values = useRecord();
const values = useRecord(); const ctx = useActionContext();
const ctx = useActionContext(); const variables = useVariables();
const variables = useVariables(); const localVariables = useLocalVariables({ currentForm: { values } as any });
const localVariables = useLocalVariables({ currentForm: { values } as any }); useEffect(() => {
useEffect(() => { field.stateOfLinkageRules = {};
field.stateOfLinkageRules = {}; linkageRules
linkageRules .filter((k) => !k.disabled)
.filter((k) => !k.disabled) .forEach((v) => {
.forEach((v) => { v.actions?.forEach((h) => {
v.actions?.forEach((h) => { linkageAction({
linkageAction({ operator: h.operator,
operator: h.operator, field,
field, condition: v.condition,
condition: v.condition, variables,
variables, localVariables,
localVariables,
});
}); });
}); });
}, [field, linkageRules, localVariables, variables]); });
return ( }, [field, linkageRules, localVariables, variables]);
<div className={actionDesignerCss}> const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
<ActionContextProvider value={{ ...ctx, visible, setVisible }}> const buttonRef = composeRef(ref, internalRef);
<CreateAction return (
{...props} //@ts-ignore
onClick={(collectionData) => { <div className={actionDesignerCss} ref={buttonRef as React.Ref<HTMLButtonElement>}>
setVisible(true); <ActionContextProvider value={{ ...ctx, visible, setVisible }}>
setCurrentCollection(collectionData.name); <CreateAction
setCurrentCollectionDataSource(collectionData.dataSource); {...props}
}} onClick={(collectionData) => {
/> setVisible(true);
<CollectionProvider_deprecated name={currentCollection} dataSource={currentCollectionDataSource}> setCurrentCollection(collectionData.name);
<RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties /> setCurrentCollectionDataSource(collectionData.dataSource);
</CollectionProvider_deprecated> }}
</ActionContextProvider> />
</div> <CollectionProvider_deprecated name={currentCollection} dataSource={currentCollectionDataSource}>
); <RecursionField schema={fieldSchema} basePath={field.address} onlyRenderProperties />
}, </CollectionProvider_deprecated>
{ displayName: 'CreateRecordAction' }, </ActionContextProvider>
); </div>
);
};
function getLinkageCollection(str, form, field) { function getLinkageCollection(str, form, field) {
const variablesCtx = { $form: form.values, $iteration: form.values }; const variablesCtx = { $form: form.values, $iteration: form.values };
@ -283,6 +284,7 @@ function FinallyButton({
designable: boolean; designable: boolean;
}) { }) {
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
if (inheritsCollections?.length > 0) { if (inheritsCollections?.length > 0) {
if (!linkageFromForm) { if (!linkageFromForm) {
return allowAddToCurrent === undefined || allowAddToCurrent ? ( return allowAddToCurrent === undefined || allowAddToCurrent ? (
@ -360,6 +362,7 @@ function FinallyButton({
onClick?.(collection); onClick?.(collection);
}} }}
style={{ style={{
...props?.style,
display: !designable && field?.data?.hidden && 'none', display: !designable && field?.data?.hidden && 'none',
opacity: designable && field?.data?.hidden && 0.1, opacity: designable && field?.data?.hidden && 0.1,
}} }}
@ -368,3 +371,5 @@ function FinallyButton({
</Button> </Button>
); );
} }
export const CreateRecordAction = forwardRef<HTMLButtonElement | HTMLAnchorElement, any>(InternalCreateRecordAction);

View File

@ -10,6 +10,7 @@ export const CreateFilterActionInitializer = (props) => {
'x-designer': 'Action.Designer', 'x-designer': 'Action.Designer',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
htmlType: 'submit',
useProps: '{{ useFilterBlockActionProps }}', useProps: '{{ useFilterBlockActionProps }}',
}, },
}; };

View File

@ -3,8 +3,9 @@ import { TableOutlined } from '@ant-design/icons';
import { useCollectionManager_deprecated } from '../../collection-manager'; import { useCollectionManager_deprecated } from '../../collection-manager';
import { useSchemaTemplateManager } from '../../schema-templates'; import { useSchemaTemplateManager } from '../../schema-templates';
import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils'; import { useRecordCollectionDataSourceItems } from '../utils';
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
import { createTableBlockUISchema } from '../../modules/blocks/data-blocks/table/createTableBlockUISchema';
/** /**
* @deprecated * @deprecated
@ -17,7 +18,7 @@ export const RecordAssociationBlockInitializer = () => {
const { getCollection } = useCollectionManager_deprecated(); const { getCollection } = useCollectionManager_deprecated();
const field = itemConfig.field; const field = itemConfig.field;
const collection = getCollection(field.target); const collection = getCollection(field.target);
const resource = `${field.collectionName}.${field.name}`; const association = `${field.collectionName}.${field.name}`;
return ( return (
<SchemaInitializerItem <SchemaInitializerItem
icon={<TableOutlined />} icon={<TableOutlined />}
@ -28,17 +29,15 @@ export const RecordAssociationBlockInitializer = () => {
insert(s); insert(s);
} else { } else {
insert( insert(
createTableBlockSchema({ createTableBlockUISchema({
rowKey: collection.filterTargetKey, rowKey: collection.filterTargetKey,
collection: field.target,
dataSource: collection.dataSource, dataSource: collection.dataSource,
resource, association: association,
association: resource,
}), }),
); );
} }
}} }}
items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, resource)} items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, association)}
/> />
); );
}; };
@ -53,9 +52,8 @@ export function useCreateAssociationTableBlock() {
const collection = getCollection(field.target); const collection = getCollection(field.target);
insert( insert(
createTableBlockSchema({ createTableBlockUISchema({
rowKey: collection.filterTargetKey, rowKey: collection.filterTargetKey,
collection: field.target,
dataSource: collection.dataSource, dataSource: collection.dataSource,
association: `${field.collectionName}.${field.name}`, association: `${field.collectionName}.${field.name}`,
}), }),

View File

@ -1181,8 +1181,6 @@ export const createFormBlockSchema = (options) => {
resource: resourceName, resource: resourceName,
collection, collection,
association, association,
// action: 'get',
// useParams: '{{ useParamsFromRecord }}',
}, },
'x-toolbar': 'BlockSchemaToolbar', 'x-toolbar': 'BlockSchemaToolbar',
...(settings ? { 'x-settings': settings } : { 'x-designer': designer }), ...(settings ? { 'x-settings': settings } : { 'x-designer': designer }),
@ -1357,6 +1355,12 @@ export const createReadPrettyFormBlockSchema = (options) => {
return schema; return schema;
}; };
/**
* @deprecated
* 使 createTableBlockUISchema
* @param options
* @returns
*/
export const createTableBlockSchema = (options) => { export const createTableBlockSchema = (options) => {
const { const {
collection, collection,
@ -1688,6 +1692,15 @@ function useAssociationFields({
const targetCollection = cm.getCollection(field.target); const targetCollection = cm.getCollection(field.target);
const title = `${compile(field.uiSchema.title || field.name)} -> ${compile(targetCollection.title)}`; const title = `${compile(field.uiSchema.title || field.name)} -> ${compile(targetCollection.title)}`;
const templates = getTemplatesByCollection(dataSource, field.target).filter((template) => { const templates = getTemplatesByCollection(dataSource, field.target).filter((template) => {
// 针对弹窗中的详情区块
if (componentName === 'ReadPrettyFormItem') {
if (['hasOne', 'belongsTo'].includes(field.type)) {
return template.componentName === 'ReadPrettyFormItem';
} else {
return template.componentName === 'Details';
}
}
return ( return (
componentName && componentName &&
template.componentName === componentName && template.componentName === componentName &&

View File

@ -56,13 +56,22 @@ export interface GeneralSchemaDesignerProps {
* @default true * @default true
*/ */
draggable?: boolean; draggable?: boolean;
showDataSource?: boolean;
} }
/** /**
* @deprecated use `SchemaToolbar` instead * @deprecated use `SchemaToolbar` instead
*/ */
export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any) => { export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any) => {
const { disableInitializer, title, template, schemaSettings, contextValue, draggable = true } = props; const {
disableInitializer,
title,
template,
schemaSettings,
contextValue,
draggable = true,
showDataSource = true,
} = props;
const { dn, designable } = useDesignable(); const { dn, designable } = useDesignable();
const field = useField(); const field = useField();
const { t } = useTranslation(); const { t } = useTranslation();
@ -112,7 +121,9 @@ export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any
<div className={classNames('general-schema-designer-title', titleCss)}> <div className={classNames('general-schema-designer-title', titleCss)}>
<Space size={2}> <Space size={2}>
<span className={'title-tag'}> <span className={'title-tag'}>
{dataSource ? `${compile(dataSource?.displayName)} > ${compile(title)}` : compile(title)} {showDataSource && dataSource
? `${compile(dataSource?.displayName)} > ${compile(title)}`
: compile(title)}
</span> </span>
{template && ( {template && (
<span className={'title-tag'}> <span className={'title-tag'}>

View File

@ -293,7 +293,7 @@ export const SchemaSettingsTemplate = function Template(props) {
const findGridSchema = (fieldSchema) => { const findGridSchema = (fieldSchema) => {
return fieldSchema.reduceProperties((buf, s) => { return fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'FormV2') { if (s['x-component'] === 'FormV2' || s['x-component'] === 'Details') {
const f = s.reduceProperties((buf, s) => { const f = s.reduceProperties((buf, s) => {
if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') { if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') {
return s; return s;
@ -310,7 +310,7 @@ const findGridSchema = (fieldSchema) => {
const findBlockTemplateSchema = (fieldSchema) => { const findBlockTemplateSchema = (fieldSchema) => {
return fieldSchema.reduceProperties((buf, s) => { return fieldSchema.reduceProperties((buf, s) => {
if (s['x-component'] === 'FormV2') { if (s['x-component'] === 'FormV2' || s['x-component'] === 'Details') {
const f = s.reduceProperties((buf, s) => { const f = s.reduceProperties((buf, s) => {
if (s['x-component'] === 'BlockTemplate') { if (s['x-component'] === 'BlockTemplate') {
return s; return s;

View File

@ -0,0 +1,156 @@
import { css } from '@emotion/css';
import { ISchema, Schema, useField, useForm } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Select } from 'antd';
import { useCollectionManager_deprecated, useDesignable } from '..';
import { SchemaSettingsModalItem } from './SchemaSettings';
const UnitConversion = ({ unitConversionType }) => {
const form = useForm();
const { t } = useTranslation();
return (
<Select
defaultValue={unitConversionType || '*'}
style={{ width: 160 }}
onChange={(value) => {
form.setValuesIn('unitConversionType', value);
}}
>
<Select.Option value="*">{t('Multiply by')}</Select.Option>
<Select.Option value="/">{t('Divide by')}</Select.Option>
</Select>
);
};
export const SchemaSettingsNumberFormat = function NumberFormatConfig(props: { fieldSchema: Schema }) {
const { fieldSchema } = props;
const field = useField();
const { dn } = useDesignable();
const { t } = useTranslation();
const { getCollectionJoinField } = useCollectionManager_deprecated();
const collectionField = getCollectionJoinField(fieldSchema?.['x-collection-field']) || {};
const { formatStyle, unitConversion, unitConversionType, separator, step, addonBefore, addonAfter } =
fieldSchema['x-component-props'] || {};
const { step: prescition } = collectionField?.uiSchema['x-component-props'] || {};
return (
<SchemaSettingsModalItem
title={t('Format')}
schema={
{
type: 'object',
properties: {
formatStyle: {
type: 'string',
default: formatStyle || 'normal',
enum: [
{
value: 'normal',
label: t('Normal'),
},
{
value: 'scientifix',
label: t('Scientifix notation'),
},
],
'x-decorator': 'FormItem',
'x-component': 'Select',
title: "{{t('Style')}}",
},
unitConversion: {
type: 'number',
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
title: "{{t('Unit conversion')}}",
default: unitConversion,
'x-component-props': {
style: { width: '100%' },
addonBefore: <UnitConversion unitConversionType={unitConversionType} />,
},
},
separator: {
type: 'string',
default: separator || '0,0.00',
enum: [
{
value: '0,0.00',
label: t('100,000.00'),
},
{
value: '0.0,00',
label: t('100.000,00'),
},
{
value: '0 0,00',
label: t('100 000.00'),
},
{
value: '0.00',
label: t('100000.00'),
},
],
'x-decorator': 'FormItem',
'x-component': 'Select',
title: "{{t('Separator')}}",
},
step: {
type: 'string',
title: '{{t("Precision")}}',
'x-component': 'Select',
'x-decorator': 'FormItem',
default: step || prescition || '1',
enum: [
{ value: '1', label: '1' },
{ value: '0.1', label: '1.0' },
{ value: '0.01', label: '1.00' },
{ value: '0.001', label: '1.000' },
{ value: '0.0001', label: '1.0000' },
{ value: '0.00001', label: '1.00000' },
],
},
addonBefore: {
type: 'string',
title: '{{t("Prefix")}}',
'x-component': 'Input',
'x-decorator': 'FormItem',
default: addonBefore,
},
addonAfter: {
type: 'string',
title: '{{t("Suffix")}}',
'x-component': 'Input',
'x-decorator': 'FormItem',
default: addonAfter,
},
},
} as ISchema
}
onSubmit={(data) => {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
schema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props'] = {
...(fieldSchema['x-component-props'] || {}),
...data,
};
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = fieldSchema['x-component-props'];
//子表格/表格区块
const parts = (field.path.entire as string).split('.');
parts.pop();
const modifiedString = parts.join('.');
field.query(`${modifiedString}.*[0:].${fieldSchema.name}`).forEach((f) => {
if (f.props.name === fieldSchema.name) {
f.setComponentProps({ ...data });
}
});
dn.emit('patch', {
schema,
});
dn.refresh();
}}
/>
);
};

View File

@ -45,6 +45,7 @@ import { selectComponentFieldSettings } from '../modules/fields/component/Select
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings'; import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings'; import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings'; import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
import { inputNumberComponentFieldSettings } from '../modules/fields/component/InputNumber/inputNumberComponentFieldSettings';
export class SchemaSettingsPlugin extends Plugin { export class SchemaSettingsPlugin extends Plugin {
async load() { async load() {
@ -95,6 +96,7 @@ export class SchemaSettingsPlugin extends Plugin {
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings); this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
this.schemaSettingsManager.add(datePickerComponentFieldSettings); this.schemaSettingsManager.add(datePickerComponentFieldSettings);
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings); this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
this.schemaSettingsManager.add(inputNumberComponentFieldSettings);
this.schemaSettingsManager.add(fileManagerComponentFieldSettings); this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
this.schemaSettingsManager.add(tagComponentFieldSettings); this.schemaSettingsManager.add(tagComponentFieldSettings);

View File

@ -1,6 +1,6 @@
{ {
"name": "create-nocobase-app", "name": "create-nocobase-app",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"main": "src/index.js", "main": "src/index.js",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {

View File

@ -1,16 +1,16 @@
{ {
"name": "@nocobase/data-source-manager", "name": "@nocobase/data-source-manager",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"dependencies": { "dependencies": {
"@nocobase/actions": "0.20.0-alpha.16", "@nocobase/actions": "0.20.0-alpha.17",
"@nocobase/cache": "0.20.0-alpha.16", "@nocobase/cache": "0.20.0-alpha.17",
"@nocobase/database": "0.20.0-alpha.16", "@nocobase/database": "0.20.0-alpha.17",
"@nocobase/resourcer": "0.20.0-alpha.16", "@nocobase/resourcer": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"jsonwebtoken": "^8.5.1" "jsonwebtoken": "^8.5.1"
}, },

View File

@ -7,3 +7,10 @@ export function parseCollectionName(collection: string) {
const dataSourceName = dataSourceCollection[0] ?? 'main'; const dataSourceName = dataSourceCollection[0] ?? 'main';
return [dataSourceName, collectionName]; return [dataSourceName, collectionName];
} }
export function joinCollectionName(dataSourceName: string, collectionName: string) {
if (!dataSourceName || dataSourceName === 'main') {
return collectionName;
}
return `${dataSourceName}:${collectionName}`;
}

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/database", "name": "@nocobase/database",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@nocobase/logger": "0.20.0-alpha.16", "@nocobase/logger": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"async-mutex": "^0.3.2", "async-mutex": "^0.3.2",
"chalk": "^4.1.1", "chalk": "^4.1.1",
"cron-parser": "4.4.0", "cron-parser": "4.4.0",

View File

@ -0,0 +1,52 @@
import { Database } from '../../database';
import { mockDatabase } from '../index';
describe('association references', () => {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
await db.clean({ drop: true });
});
afterEach(async () => {
await db.close();
});
it('should add reference with default priority', async () => {
const User = db.collection({
name: 'users',
fields: [{ type: 'hasOne', name: 'profile' }],
});
const Profile = db.collection({
name: 'profiles',
fields: [{ type: 'belongsTo', name: 'user' }],
});
await db.sync();
const references = db.referenceMap.getReferences('users');
expect(references[0].priority).toBe('default');
});
it('should add reference with user defined priority', async () => {
const User = db.collection({
name: 'users',
fields: [{ type: 'hasOne', name: 'profile', onDelete: 'CASCADE' }],
});
const Profile = db.collection({
name: 'profiles',
fields: [{ type: 'belongsTo', name: 'user' }],
});
await db.sync();
const references = db.referenceMap.getReferences('users');
expect(references.length).toBe(1);
expect(references[0].priority).toBe('user');
});
});

View File

@ -14,6 +14,36 @@ describe('belongs to field', () => {
await db.close(); await db.close();
}); });
it('should load with no action', async () => {
const User = db.collection({
name: 'users',
fields: [{ type: 'string', name: 'name', unique: true }],
});
const Post = db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user', onDelete: 'NO ACTION' },
],
});
await db.sync();
const u1 = await User.repository.create({ values: { name: 'u1' } });
const p1 = await Post.repository.create({ values: { title: 'p1', user: u1.id } });
// delete u1
await User.repository.destroy({ filterByTk: u1.id });
// list posts with user
const post = await Post.repository.findOne({
appends: ['user'],
});
expect(post.user).toBeNull();
});
it('should throw error when associated with item that null with target key', async () => { it('should throw error when associated with item that null with target key', async () => {
const User = db.collection({ const User = db.collection({
name: 'users', name: 'users',

View File

@ -27,7 +27,7 @@ import { CollectionFactory } from './collection-factory';
import { CollectionGroupManager } from './collection-group-manager'; import { CollectionGroupManager } from './collection-group-manager';
import { ImporterReader, ImportFileExtension } from './collection-importer'; import { ImporterReader, ImportFileExtension } from './collection-importer';
import DatabaseUtils from './database-utils'; import DatabaseUtils from './database-utils';
import ReferencesMap from './features/ReferencesMap'; import ReferencesMap from './features/references-map';
import { referentialIntegrityCheck } from './features/referential-integrity-check'; import { referentialIntegrityCheck } from './features/referential-integrity-check';
import { ArrayFieldRepository } from './field-repository/array-field-repository'; import { ArrayFieldRepository } from './field-repository/array-field-repository';
import * as FieldTypes from './fields'; import * as FieldTypes from './fields';

View File

@ -1,76 +0,0 @@
export interface Reference {
sourceCollectionName: string;
sourceField: string;
targetField: string;
targetCollectionName: string;
onDelete: string;
}
class ReferencesMap {
protected map: Map<string, Reference[]> = new Map();
addReference(reference: Reference) {
if (!reference.onDelete) {
reference.onDelete = 'SET NULL';
}
reference.onDelete = reference.onDelete.toUpperCase();
const existReference = this.existReference(reference);
if (existReference && existReference.onDelete !== reference.onDelete) {
if (reference.onDelete === 'SET NULL') {
// using existing reference
return;
} else if (existReference.onDelete === 'SET NULL') {
existReference.onDelete = reference.onDelete;
} else {
throw new Error(
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
reference,
)}`,
);
}
}
if (!existReference) {
this.map.set(reference.targetCollectionName, [
...(this.map.get(reference.targetCollectionName) || []),
reference,
]);
}
}
getReferences(collectionName) {
return this.map.get(collectionName);
}
existReference(reference: Reference) {
const references = this.map.get(reference.targetCollectionName);
if (!references) {
return null;
}
const keys = Object.keys(reference).filter((k) => k !== 'onDelete');
return references.find((ref) => keys.every((key) => ref[key] === reference[key]));
}
removeReference(reference: Reference) {
const references = this.map.get(reference.targetCollectionName);
if (!references) {
return;
}
const keys = ['sourceCollectionName', 'sourceField', 'targetField', 'targetCollectionName'];
this.map.set(
reference.targetCollectionName,
references.filter((ref) => !keys.every((key) => ref[key] === reference[key])),
);
}
}
export default ReferencesMap;

View File

@ -0,0 +1,107 @@
export type ReferencePriority = 'default' | 'user';
export interface Reference {
sourceCollectionName: string;
sourceField: string;
targetField: string;
targetCollectionName: string;
onDelete: string;
priority: ReferencePriority;
}
const DEFAULT_ON_DELETE = 'NO ACTION';
export function buildReference(options: Partial<Reference>): Reference {
const { sourceCollectionName, sourceField, targetField, targetCollectionName, onDelete, priority } = options;
return {
sourceCollectionName,
sourceField,
targetField,
targetCollectionName,
onDelete: (onDelete || DEFAULT_ON_DELETE).toUpperCase(),
priority: assignPriority(priority, onDelete),
};
}
function assignPriority(priority: string | undefined, onDelete: string | undefined): ReferencePriority {
if (priority) {
return priority as ReferencePriority;
}
return onDelete ? 'user' : 'default';
}
const PRIORITY_MAP = {
default: 1,
user: 2,
};
class ReferencesMap {
protected map: Map<string, Reference[]> = new Map();
addReference(reference: Reference) {
const existReference = this.existReference(reference);
if (existReference && existReference.onDelete !== reference.onDelete) {
// check two references onDelete priority, using the higher priority, if both are the same, throw error
const existPriority = PRIORITY_MAP[existReference.priority];
const newPriority = PRIORITY_MAP[reference.priority];
if (newPriority > existPriority) {
existReference.onDelete = reference.onDelete;
existReference.priority = reference.priority;
} else if (newPriority === existPriority && newPriority === PRIORITY_MAP['user']) {
if (existReference.onDelete === 'SET NULL' && reference.onDelete === 'CASCADE') {
existReference.onDelete = reference.onDelete;
} else {
console.error(new Error(
`On Delete Conflict, exist reference ${JSON.stringify(existReference)}, new reference ${JSON.stringify(
reference,
)}`,
));
}
}
}
if (!existReference) {
this.map.set(reference.targetCollectionName, [
...(this.map.get(reference.targetCollectionName) || []),
reference,
]);
}
}
getReferences(collectionName) {
return this.map.get(collectionName);
}
existReference(reference: Reference) {
const references = this.map.get(reference.targetCollectionName);
if (!references) {
return null;
}
const keys = Object.keys(reference).filter((k) => k !== 'onDelete' && k !== 'priority');
return references.find((ref) => keys.every((key) => ref[key] === reference[key]));
}
removeReference(reference: Reference) {
const references = this.map.get(reference.targetCollectionName);
if (!references) {
return;
}
const keys = ['sourceCollectionName', 'sourceField', 'targetField', 'targetCollectionName'];
this.map.set(
reference.targetCollectionName,
references.filter((ref) => !keys.every((key) => ref[key] === reference[key])),
);
}
}
export default ReferencesMap;

View File

@ -21,6 +21,11 @@ export async function referentialIntegrityCheck(options: ReferentialIntegrityChe
for (const reference of references) { for (const reference of references) {
const { sourceCollectionName, sourceField, targetField, onDelete } = reference; const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
if (onDelete === 'NO ACTION') {
continue;
}
const sourceCollection = db.collections.get(sourceCollectionName); const sourceCollection = db.collections.get(sourceCollectionName);
const sourceRepository = sourceCollection.repository; const sourceRepository = sourceCollection.repository;

View File

@ -1,6 +1,6 @@
import lodash, { omit } from 'lodash'; import lodash, { omit } from 'lodash';
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize'; import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
import { Reference } from '../features/ReferencesMap'; import { buildReference, Reference, ReferencePriority } from '../features/references-map';
import { checkIdentifier } from '../utils'; import { checkIdentifier } from '../utils';
import { BaseRelationFieldOptions, RelationField } from './relation-field'; import { BaseRelationFieldOptions, RelationField } from './relation-field';
@ -16,20 +16,26 @@ export class BelongsToField extends RelationField {
return target || Utils.pluralize(name); return target || Utils.pluralize(name);
} }
static toReference(db, association, onDelete) { static toReference(db, association, onDelete, priority: ReferencePriority = 'default'): Reference {
const targetKey = association.targetKey; const targetKey = association.targetKey;
return { return buildReference({
sourceCollectionName: db.modelCollection.get(association.source).name, sourceCollectionName: db.modelCollection.get(association.source).name,
sourceField: association.foreignKey, sourceField: association.foreignKey,
targetField: targetKey, targetField: targetKey,
targetCollectionName: db.modelCollection.get(association.target).name, targetCollectionName: db.modelCollection.get(association.target).name,
onDelete: onDelete, onDelete: onDelete,
}; priority: priority,
});
} }
reference(association): Reference { reference(association): Reference {
return BelongsToField.toReference(this.database, association, this.options.onDelete); return BelongsToField.toReference(
this.database,
association,
this.options.onDelete,
this.options.onDelete ? 'user' : 'default',
);
} }
checkAssociationKeys() { checkAssociationKeys() {

View File

@ -1,7 +1,7 @@
import { omit } from 'lodash'; import { omit } from 'lodash';
import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize'; import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Reference } from '../features/ReferencesMap'; import { Reference } from '../features/references-map';
import { checkIdentifier } from '../utils'; import { checkIdentifier } from '../utils';
import { BelongsToField } from './belongs-to-field'; import { BelongsToField } from './belongs-to-field';
import { MultipleRelationFieldOptions, RelationField } from './relation-field'; import { MultipleRelationFieldOptions, RelationField } from './relation-field';
@ -32,6 +32,8 @@ export class BelongsToManyField extends RelationField {
const onDelete = this.options.onDelete || 'CASCADE'; const onDelete = this.options.onDelete || 'CASCADE';
const priority = this.options.onDelete ? 'user' : 'default';
const targetAssociation = association.toTarget; const targetAssociation = association.toTarget;
if (association.targetKey) { if (association.targetKey) {
@ -45,8 +47,8 @@ export class BelongsToManyField extends RelationField {
} }
return [ return [
BelongsToField.toReference(db, targetAssociation, onDelete), BelongsToField.toReference(db, targetAssociation, onDelete, priority),
BelongsToField.toReference(db, sourceAssociation, onDelete), BelongsToField.toReference(db, sourceAssociation, onDelete, priority),
]; ];
} }
@ -149,10 +151,6 @@ export class BelongsToManyField extends RelationField {
Object.defineProperty(Through.model, 'isThrough', { value: true }); Object.defineProperty(Through.model, 'isThrough', { value: true });
} }
if (!this.options.onDelete) {
this.options.onDelete = 'CASCADE';
}
const belongsToManyOptions = { const belongsToManyOptions = {
constraints: false, constraints: false,
...omit(this.options, ['name', 'type', 'target']), ...omit(this.options, ['name', 'type', 'target']),

View File

@ -8,7 +8,7 @@ import {
Utils, Utils,
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Reference } from '../features/ReferencesMap'; import { buildReference, Reference } from '../features/references-map';
import { checkIdentifier } from '../utils'; import { checkIdentifier } from '../utils';
import { MultipleRelationFieldOptions, RelationField } from './relation-field'; import { MultipleRelationFieldOptions, RelationField } from './relation-field';
@ -89,13 +89,13 @@ export class HasManyField extends RelationField {
reference(association): Reference { reference(association): Reference {
const sourceKey = association.sourceKey; const sourceKey = association.sourceKey;
return { return buildReference({
sourceCollectionName: this.database.modelCollection.get(association.target).name, sourceCollectionName: this.database.modelCollection.get(association.target).name,
sourceField: association.foreignKey, sourceField: association.foreignKey,
targetField: sourceKey, targetField: sourceKey,
targetCollectionName: this.database.modelCollection.get(association.source).name, targetCollectionName: this.database.modelCollection.get(association.source).name,
onDelete: this.options.onDelete, onDelete: this.options.onDelete,
}; });
} }
checkAssociationKeys() { checkAssociationKeys() {

View File

@ -8,7 +8,7 @@ import {
Utils, Utils,
} from 'sequelize'; } from 'sequelize';
import { Collection } from '../collection'; import { Collection } from '../collection';
import { Reference } from '../features/ReferencesMap'; import { buildReference, Reference } from '../features/references-map';
import { checkIdentifier } from '../utils'; import { checkIdentifier } from '../utils';
import { BaseRelationFieldOptions, RelationField } from './relation-field'; import { BaseRelationFieldOptions, RelationField } from './relation-field';
@ -98,13 +98,13 @@ export class HasOneField extends RelationField {
reference(association): Reference { reference(association): Reference {
const sourceKey = association.sourceKey; const sourceKey = association.sourceKey;
return { return buildReference({
sourceCollectionName: this.database.modelCollection.get(association.target).name, sourceCollectionName: this.database.modelCollection.get(association.target).name,
sourceField: association.foreignKey, sourceField: association.foreignKey,
targetField: sourceKey, targetField: sourceKey,
targetCollectionName: this.database.modelCollection.get(association.source).name, targetCollectionName: this.database.modelCollection.get(association.source).name,
onDelete: this.options.onDelete, onDelete: this.options.onDelete,
}; });
} }
checkAssociationKeys() { checkAssociationKeys() {

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/devtools", "name": "@nocobase/devtools",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./src/index.js", "main": "./src/index.js",
"dependencies": { "dependencies": {
"@nocobase/build": "0.20.0-alpha.16", "@nocobase/build": "0.20.0-alpha.17",
"@nocobase/client": "0.20.0-alpha.16", "@nocobase/client": "0.20.0-alpha.17",
"@nocobase/test": "0.20.0-alpha.16", "@nocobase/test": "0.20.0-alpha.17",
"@types/koa": "^2.13.4", "@types/koa": "^2.13.4",
"@types/koa-bodyparser": "^4.3.4", "@types/koa-bodyparser": "^4.3.4",
"@types/lodash": "^4.14.177", "@types/lodash": "^4.14.177",

View File

@ -1,13 +1,13 @@
{ {
"name": "@nocobase/evaluators", "name": "@nocobase/evaluators",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@formulajs/formulajs": "4.2.0", "@formulajs/formulajs": "4.2.0",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"mathjs": "^10.6.0" "mathjs": "^10.6.0"
}, },
"repository": { "repository": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/logger", "name": "@nocobase/logger",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "nocobase logging library", "description": "nocobase logging library",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",

View File

@ -1,12 +1,12 @@
{ {
"name": "@nocobase/resourcer", "name": "@nocobase/resourcer",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "", "description": "",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"koa-compose": "^4.1.0", "koa-compose": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",

View File

@ -1,10 +1,9 @@
import { assign, MergeStrategies, prePerfHooksWrap, requireModule } from '@nocobase/utils'; import { assign, MergeStrategies, requireModule } from '@nocobase/utils';
import compose from 'koa-compose'; import compose from 'koa-compose';
import _ from 'lodash'; import _ from 'lodash';
import Middleware, { MiddlewareType } from './middleware'; import Middleware, { MiddlewareType } from './middleware';
import Resource from './resource'; import Resource from './resource';
import { HandlerType } from './resourcer'; import { HandlerType } from './resourcer';
import { RecordableHistogram, performance } from 'perf_hooks';
export type ActionType = string | HandlerType | ActionOptions; export type ActionType = string | HandlerType | ActionOptions;
@ -161,27 +160,38 @@ export interface ActionParams {
*/ */
values?: any; values?: any;
/** /**
* Model * This method is deprecated and should not be used.
* Use {@link action.resourceName.split(',')[0]} instead.
* @deprecated
*/ */
resourceName?: string; resourceName?: string;
/** /**
* * This method is deprecated and should not be used.
* Use {@link filterByTk} instead.
* @deprecated
*/ */
resourceIndex?: string; resourceIndex?: string;
/** /**
* * This method is deprecated and should not be used.
* Use {@link action.resourceName.split(',')[1]} instead.
* @deprecated
*/ */
associatedName?: string; associatedName?: string;
/** /**
* * This method is deprecated and should not be used.
* Use {@link action.sourceId} instead.
* @deprecated
*/ */
associatedIndex?: string; associatedIndex?: string;
/** /**
* * This method is deprecated and should not be used.
* @deprecated
*/ */
associated?: any; associated?: any;
/** /**
* * This method is deprecated and should not be used.
* Use {@link action.actionName} instead.
* @deprecated
*/ */
actionName?: string; actionName?: string;
/** /**
@ -204,11 +214,23 @@ export class Action {
public params: ActionParams = {}; public params: ActionParams = {};
public actionName: string; public actionName: string;
public resourceName: string; public resourceName: string;
/**
* This method is deprecated and should not be used.
* Use {@link this.sourceId} instead.
* @deprecated
*/
public resourceOf: any; public resourceOf: any;
public sourceId: any;
public readonly middlewares: Array<Middleware> = []; public readonly middlewares: Array<Middleware> = [];
/**
* @internal
*/
constructor(options: ActionOptions) { constructor(options: ActionOptions) {
options = requireModule(options); options = requireModule(options);
if (typeof options === 'function') { if (typeof options === 'function') {
@ -221,15 +243,22 @@ export class Action {
this.mergeParams(params); this.mergeParams(params);
} }
/**
* @internal
*/
toJSON() { toJSON() {
return { return {
actionName: this.actionName, actionName: this.actionName,
resourceName: this.resourceName, resourceName: this.resourceName,
resourceOf: this.resourceOf, resourceOf: this.sourceId,
sourceId: this.sourceId,
params: this.params, params: this.params,
}; };
} }
/**
* @internal
*/
clone() { clone() {
const options = _.cloneDeep(this.options); const options = _.cloneDeep(this.options);
delete options.middleware; delete options.middleware;
@ -241,6 +270,9 @@ export class Action {
return action; return action;
} }
/**
* @internal
*/
setContext(context: any) { setContext(context: any) {
this.context = context; this.context = context;
} }
@ -266,34 +298,55 @@ export class Action {
}); });
} }
/**
* @internal
*/
setResource(resource: Resource) { setResource(resource: Resource) {
this.resource = resource; this.resource = resource;
return this; return this;
} }
/**
* @internal
*/
getResource() { getResource() {
return this.resource; return this.resource;
} }
/**
* @internal
*/
getOptions(): ActionOptions { getOptions(): ActionOptions {
return this.options; return this.options;
} }
/**
* @internal
*/
setName(name: ActionName) { setName(name: ActionName) {
this.name = name; this.name = name;
return this; return this;
} }
/**
* @internal
*/
getName() { getName() {
return this.name; return this.name;
} }
/**
* @internal
*/
getMiddlewareHandlers() { getMiddlewareHandlers() {
return this.middlewares return this.middlewares
.filter((middleware) => middleware.canAccess(this.name)) .filter((middleware) => middleware.canAccess(this.name))
.map((middleware) => middleware.getHandler()); .map((middleware) => middleware.getHandler());
} }
/**
* @internal
*/
getHandler() { getHandler() {
const handler = requireModule(this.handler || this.resource.resourcer.getRegisteredHandler(this.name)); const handler = requireModule(this.handler || this.resource.resourcer.getRegisteredHandler(this.name));
if (typeof handler !== 'function') { if (typeof handler !== 'function') {
@ -303,6 +356,9 @@ export class Action {
return handler; return handler;
} }
/**
* @internal
*/
getHandlers() { getHandlers() {
const handlers = [ const handlers = [
...this.resource.resourcer.getMiddlewares(), ...this.resource.resourcer.getMiddlewares(),
@ -315,10 +371,16 @@ export class Action {
return handlers; return handlers;
} }
/**
* @internal
*/
async execute(context: any, next?: any) { async execute(context: any, next?: any) {
return await compose(this.getHandlers())(context, next); return await compose(this.getHandlers())(context, next);
} }
/**
* @internal
*/
static toInstanceMap(actions: object, resource?: Resource) { static toInstanceMap(actions: object, resource?: Resource) {
return new Map( return new Map(
Object.entries(actions).map(([key, options]) => { Object.entries(actions).map(([key, options]) => {

View File

@ -150,6 +150,9 @@ export interface ImportOptions {
} }
export class Resourcer { export class Resourcer {
/**
* @internal
*/
public readonly options: ResourcerOptions; public readonly options: ResourcerOptions;
protected resources = new Map<string, Resource>(); protected resources = new Map<string, Resource>();
/** /**
@ -173,6 +176,7 @@ export class Resourcer {
* @param {object} [options] * @param {object} [options]
* @param {string} [options.directory] * @param {string} [options.directory]
* @param {array} [options.extensions = ['js', 'ts', 'json']] * @param {array} [options.extensions = ['js', 'ts', 'json']]
*
*/ */
public async import(options: ImportOptions): Promise<Map<string, Resource>> { public async import(options: ImportOptions): Promise<Map<string, Resource>> {
const { extensions = ['js', 'ts', 'json'], directory } = options; const { extensions = ['js', 'ts', 'json'], directory } = options;
@ -206,14 +210,27 @@ export class Resourcer {
return this.resources.has(name); return this.resources.has(name);
} }
/**
* @internal
*/
removeResource(name) { removeResource(name) {
return this.resources.delete(name); return this.resources.delete(name);
} }
/**
* This method is deprecated and should not be used.
* Use {@link this.registerActionHandler()} instead.
* @deprecated
*/
registerAction(name: ActionName, handler: HandlerType) { registerAction(name: ActionName, handler: HandlerType) {
this.registerActionHandler(name, handler); this.registerActionHandler(name, handler);
} }
/**
* This method is deprecated and should not be used.
* Use {@link this.registerActionHandlers()} instead.
* @deprecated
*/
registerActions(handlers: Handlers) { registerActions(handlers: Handlers) {
this.registerActionHandlers(handlers); this.registerActionHandlers(handlers);
} }
@ -233,14 +250,23 @@ export class Resourcer {
this.actionHandlers.set(name, handler); this.actionHandlers.set(name, handler);
} }
/**
* @internal
*/
getRegisteredHandler(name: ActionName) { getRegisteredHandler(name: ActionName) {
return this.actionHandlers.get(name); return this.actionHandlers.get(name);
} }
/**
* @internal
*/
getRegisteredHandlers() { getRegisteredHandlers() {
return this.actionHandlers; return this.actionHandlers;
} }
/**
* @internal
*/
getResource(name: string): Resource { getResource(name: string): Resource {
if (!this.resources.has(name)) { if (!this.resources.has(name)) {
throw new Error(`${name} resource does not exist`); throw new Error(`${name} resource does not exist`);
@ -248,6 +274,9 @@ export class Resourcer {
return this.resources.get(name); return this.resources.get(name);
} }
/**
* @internal
*/
getAction(name: string, action: ActionName): Action { getAction(name: string, action: ActionName): Action {
// 支持注册局部 action // 支持注册局部 action
if (this.actionHandlers.has(`${name}:${action}`)) { if (this.actionHandlers.has(`${name}:${action}`)) {
@ -256,6 +285,9 @@ export class Resourcer {
return this.getResource(name).getAction(action); return this.getResource(name).getAction(action);
} }
/**
* @internal
*/
getMiddlewares() { getMiddlewares() {
return this.middlewares.nodes; return this.middlewares.nodes;
} }
@ -264,6 +296,11 @@ export class Resourcer {
this.middlewares.add(middlewares, options); this.middlewares.add(middlewares, options);
} }
/**
* This method is deprecated and should not be used.
* Use {@link this.middleware()} instead.
* @deprecated
*/
restApiMiddleware({ prefix, accessors, skipIfDataSourceExists = false }: KoaMiddlewareOptions = {}) { restApiMiddleware({ prefix, accessors, skipIfDataSourceExists = false }: KoaMiddlewareOptions = {}) {
return async (ctx: ResourcerContext, next: () => Promise<any>) => { return async (ctx: ResourcerContext, next: () => Promise<any>) => {
if (skipIfDataSourceExists) { if (skipIfDataSourceExists) {
@ -317,6 +354,7 @@ export class Resourcer {
ctx.action.setContext(ctx); ctx.action.setContext(ctx);
ctx.action.actionName = params.actionName; ctx.action.actionName = params.actionName;
ctx.action.sourceId = params.associatedIndex;
ctx.action.resourceOf = params.associatedIndex; ctx.action.resourceOf = params.associatedIndex;
ctx.action.resourceName = params.associatedName ctx.action.resourceName = params.associatedName
? `${params.associatedName}.${params.resourceName}` ? `${params.associatedName}.${params.resourceName}`
@ -349,11 +387,7 @@ export class Resourcer {
} }
/** /**
* API * @internal
*
* @param options
* @param context
* @param next
*/ */
async execute(options: ExecuteOptions, context: ResourcerContext = {}, next?: any) { async execute(options: ExecuteOptions, context: ResourcerContext = {}, next?: any) {
const { resource, action } = options; const { resource, action } = options;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/sdk", "name": "@nocobase/sdk",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/server", "name": "@nocobase/server",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"main": "lib/index.js", "main": "lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -10,18 +10,18 @@
"@koa/cors": "^3.1.0", "@koa/cors": "^3.1.0",
"@koa/multer": "^3.0.2", "@koa/multer": "^3.0.2",
"@koa/router": "^9.4.0", "@koa/router": "^9.4.0",
"@nocobase/acl": "0.20.0-alpha.16", "@nocobase/acl": "0.20.0-alpha.17",
"@nocobase/actions": "0.20.0-alpha.16", "@nocobase/actions": "0.20.0-alpha.17",
"@nocobase/auth": "0.20.0-alpha.16", "@nocobase/auth": "0.20.0-alpha.17",
"@nocobase/cache": "0.20.0-alpha.16", "@nocobase/cache": "0.20.0-alpha.17",
"@nocobase/data-source-manager": "0.20.0-alpha.16", "@nocobase/data-source-manager": "0.20.0-alpha.17",
"@nocobase/database": "0.20.0-alpha.16", "@nocobase/database": "0.20.0-alpha.17",
"@nocobase/evaluators": "0.20.0-alpha.16", "@nocobase/evaluators": "0.20.0-alpha.17",
"@nocobase/logger": "0.20.0-alpha.16", "@nocobase/logger": "0.20.0-alpha.17",
"@nocobase/resourcer": "0.20.0-alpha.16", "@nocobase/resourcer": "0.20.0-alpha.17",
"@nocobase/sdk": "0.20.0-alpha.16", "@nocobase/sdk": "0.20.0-alpha.17",
"@nocobase/telemetry": "0.20.0-alpha.16", "@nocobase/telemetry": "0.20.0-alpha.17",
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"@types/decompress": "4.2.4", "@types/decompress": "4.2.4",
"@types/ini": "^1.3.31", "@types/ini": "^1.3.31",
"@types/koa-send": "^4.1.3", "@types/koa-send": "^4.1.3",

View File

@ -142,19 +142,41 @@ export type MaintainingCommandStatus = {
}; };
export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter { export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter {
/**
* @internal
*/
declare middleware: any; declare middleware: any;
/**
* @internal
*/
stopped = false; stopped = false;
/**
* @internal
*/
ready = false; ready = false;
declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>; declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
/**
* @internal
*/
public rawOptions: ApplicationOptions; public rawOptions: ApplicationOptions;
/**
* @internal
*/
public activatedCommand: { public activatedCommand: {
name: string; name: string;
} = null; } = null;
/**
* @internal
*/
public running = false; public running = false;
/**
* @internal
*/
public perfHistograms = new Map<string, RecordableHistogram>(); public perfHistograms = new Map<string, RecordableHistogram>();
protected plugins = new Map<string, Plugin>(); protected plugins = new Map<string, Plugin>();
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance(); protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
protected _started: boolean; protected _started: boolean;
protected _logger: SystemLogger;
private _authenticated = false; private _authenticated = false;
private _maintaining = false; private _maintaining = false;
private _maintainingCommandStatus: MaintainingCommandStatus; private _maintainingCommandStatus: MaintainingCommandStatus;
@ -193,12 +215,18 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
protected _loaded: boolean; protected _loaded: boolean;
/**
* @internal
*/
get loaded() { get loaded() {
return this._loaded; return this._loaded;
} }
private _maintainingMessage: string; private _maintainingMessage: string;
/**
* @internal
*/
get maintainingMessage() { get maintainingMessage() {
return this._maintainingMessage; return this._maintainingMessage;
} }
@ -222,8 +250,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this.mainDataSource.collectionManager.db; return this.mainDataSource.collectionManager.db;
} }
protected _logger: SystemLogger;
get logger() { get logger() {
return this._logger; return this._logger;
} }
@ -244,6 +270,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._cache; return this._cache;
} }
/**
* @internal
*/
set cache(cache: Cache) { set cache(cache: Cache) {
this._cache = cache; this._cache = cache;
} }
@ -278,6 +307,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
protected _locales: Locale; protected _locales: Locale;
/**
* This method is deprecated and should not be used.
* Use {@link #localeManager} instead.
* @deprecated
*/
get locales() { get locales() {
return this._locales; return this._locales;
} }
@ -312,10 +346,16 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._dataSourceManager; return this._dataSourceManager;
} }
/**
* @internal
*/
getMaintaining() { getMaintaining() {
return this._maintainingCommandStatus; return this._maintainingCommandStatus;
} }
/**
* @internal
*/
setMaintaining(_maintainingCommandStatus: MaintainingCommandStatus) { setMaintaining(_maintainingCommandStatus: MaintainingCommandStatus) {
this._maintainingCommandStatus = _maintainingCommandStatus; this._maintainingCommandStatus = _maintainingCommandStatus;
@ -329,6 +369,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._maintaining = true; this._maintaining = true;
} }
/**
* @internal
*/
setMaintainingMessage(message: string) { setMaintainingMessage(message: string) {
this._maintainingMessage = message; this._maintainingMessage = message;
@ -338,11 +381,18 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
}); });
} }
/**
* This method is deprecated and should not be used.
* Use {@link #this.version.get()} instead.
* @deprecated
*/
getVersion() { getVersion() {
return packageJson.version; return packageJson.version;
} }
/** /**
* This method is deprecated and should not be used.
* Use {@link #this.pm.addPreset()} instead.
* @deprecated * @deprecated
*/ */
plugin<O = any>(pluginClass: any, options?: O) { plugin<O = any>(pluginClass: any, options?: O) {
@ -359,6 +409,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this; return this;
} }
/**
* @internal
*/
callback() { callback() {
const fn = compose(this.middleware.nodes); const fn = compose(this.middleware.nodes);
@ -372,14 +425,29 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
}; };
} }
/**
* This method is deprecated and should not be used.
* Use {@link #this.db.collection()} instead.
* @deprecated
*/
collection(options: CollectionOptions) { collection(options: CollectionOptions) {
return this.db.collection(options); return this.db.collection(options);
} }
/**
* This method is deprecated and should not be used.
* Use {@link #this.resourcer.define()} instead.
* @deprecated
*/
resource(options: ResourceOptions) { resource(options: ResourceOptions) {
return this.resourcer.define(options); return this.resourcer.define(options);
} }
/**
* This method is deprecated and should not be used.
* Use {@link #this.resourcer.registerActions()} instead.
* @deprecated
*/
actions(handlers: any, options?: ActionsOptions) { actions(handlers: any, options?: ActionsOptions) {
return this.resourcer.registerActions(handlers); return this.resourcer.registerActions(handlers);
} }
@ -392,11 +460,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return (this.cli as any)._findCommand(name); return (this.cli as any)._findCommand(name);
} }
async preload() { /**
// load core collections * @internal
// load plugin commands */
}
async reInit() { async reInit() {
if (!this._loaded) { if (!this._loaded) {
return; return;
@ -500,12 +566,19 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
} }
/** /**
* This method is deprecated and should not be used.
* Use {@link this.pm.get()} instead.
* @deprecated * @deprecated
*/ */
getPlugin<P extends Plugin>(name: string | typeof Plugin) { getPlugin<P extends Plugin>(name: string | typeof Plugin) {
return this.pm.get(name) as P; return this.pm.get(name) as P;
} }
/**
* This method is deprecated and should not be used.
* Use {@link this.runAsCLI()} instead.
* @deprecated
*/
async parse(argv = process.argv) { async parse(argv = process.argv) {
return this.runAsCLI(argv); return this.runAsCLI(argv);
} }
@ -528,7 +601,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return await this.runAsCLI([command, ...args], { from: 'user', throwError: true }); return await this.runAsCLI([command, ...args], { from: 'user', throwError: true });
} }
createCli() { protected createCLI() {
const command = new AppCommand('nocobase') const command = new AppCommand('nocobase')
.usage('[command] [options]') .usage('[command] [options]')
.hook('preAction', async (_, actionCommand) => { .hook('preAction', async (_, actionCommand) => {
@ -568,6 +641,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return command; return command;
} }
/**
* @internal
*/
async loadMigrations(options) { async loadMigrations(options) {
const { directory, context, namespace } = options; const { directory, context, namespace } = options;
const migrations = { const migrations = {
@ -594,6 +670,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return migrations; return migrations;
} }
/**
* @internal
*/
async loadCoreMigrations() { async loadCoreMigrations() {
const migrations = await this.loadMigrations({ const migrations = await this.loadMigrations({
directory: resolve(__dirname, 'migrations'), directory: resolve(__dirname, 'migrations'),
@ -624,11 +703,17 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
}; };
} }
/**
* @internal
*/
async loadPluginCommands() { async loadPluginCommands() {
this.log.debug('load plugin commands'); this.log.debug('load plugin commands');
await this.pm.loadCommands(); await this.pm.loadCommands();
} }
/**
* @internal
*/
async runAsCLI( async runAsCLI(
argv = process.argv, argv = process.argv,
options?: ParseOptions & { throwError?: boolean; reqId?: string }, options?: ParseOptions & { throwError?: boolean; reqId?: string },
@ -732,6 +817,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this.stopped = false; this.stopped = false;
} }
/**
* @internal
*/
async emitStartedEvent(options: StartOptions = {}) { async emitStartedEvent(options: StartOptions = {}) {
await this.emitAsync('__started', this, { await this.emitAsync('__started', this, {
maintainingStatus: lodash.cloneDeep(this._maintainingCommandStatus), maintainingStatus: lodash.cloneDeep(this._maintainingCommandStatus),
@ -743,6 +831,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
return this._started; return this._started;
} }
/**
* @internal
*/
async tryReloadOrRestart(options: StartOptions = {}) { async tryReloadOrRestart(options: StartOptions = {}) {
if (this._started) { if (this._started) {
await this.restart(options); await this.restart(options);
@ -769,11 +860,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
const log = const log =
options.logging === false options.logging === false
? { ? {
debug() {}, debug() { },
warn() {}, warn() { },
info() {}, info() { },
error() {}, error() { },
} }
: this.log; : this.log;
log.debug('stop app...', { method: 'stop' }); log.debug('stop app...', { method: 'stop' });
this.setMaintainingMessage('stopping app...'); this.setMaintainingMessage('stopping app...');
@ -941,6 +1032,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
}; };
} }
/**
* @internal
*/
reInitEvents() { reInitEvents() {
for (const eventName of this.eventNames()) { for (const eventName of this.eventNames()) {
for (const listener of this.listeners(eventName)) { for (const listener of this.listeners(eventName)) {
@ -986,7 +1080,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
this._cronJobManager = new CronJobManager(this); this._cronJobManager = new CronJobManager(this);
this._cli = this.createCli(); this._cli = this.createCLI();
this._i18n = createI18n(options); this._i18n = createI18n(options);
this.context.db = this.db; this.context.db = this.db;

View File

@ -67,6 +67,9 @@ export class Locale {
async getCacheResources(lang: string) { async getCacheResources(lang: string) {
this.resourceCached.set(lang, true); this.resourceCached.set(lang, true);
if (process.env.APP_ENV !== 'production') {
await this.cache.reset();
}
return await this.wrapCache(`resources:${lang}`, () => this.getResources(lang)); return await this.wrapCache(`resources:${lang}`, () => this.getResources(lang));
} }

View File

@ -25,7 +25,10 @@ export const getResource = (packageName: string, lang: string, isPlugin = true)
for (const prefix of prefixes) { for (const prefix of prefixes) {
try { try {
const file = `${packageName}/${prefix}/locale/${lang}`; const file = `${packageName}/${prefix}/locale/${lang}`;
require.resolve(file); const f = require.resolve(file);
if (process.env.APP_ENV !== 'production') {
delete require.cache[f];
}
const resource = requireModule(file); const resource = requireModule(file);
resources.push(resource); resources.push(resource);
} catch (error) { } catch (error) {

View File

@ -3,12 +3,21 @@ import lodash from 'lodash';
import { PluginManager } from './plugin-manager'; import { PluginManager } from './plugin-manager';
export class PluginManagerRepository extends Repository { export class PluginManagerRepository extends Repository {
/**
* @internal
*/
pm: PluginManager; pm: PluginManager;
/**
* @internal
*/
setPluginManager(pm: PluginManager) { setPluginManager(pm: PluginManager) {
this.pm = pm; this.pm = pm;
} }
/**
* @deprecated
*/
async remove(name: string | string[]) { async remove(name: string | string[]) {
await this.destroy({ await this.destroy({
filter: { filter: {
@ -17,6 +26,9 @@ export class PluginManagerRepository extends Repository {
}); });
} }
/**
* @deprecated
*/
async enable(name: string | string[]) { async enable(name: string | string[]) {
const pluginNames = lodash.castArray(name); const pluginNames = lodash.castArray(name);
const plugins = pluginNames.map((name) => this.pm.get(name)); const plugins = pluginNames.map((name) => this.pm.get(name));
@ -56,6 +68,9 @@ export class PluginManagerRepository extends Repository {
} }
} }
/**
* @deprecated
*/
async disable(name: string | string[]) { async disable(name: string | string[]) {
name = lodash.cloneDeep(name); name = lodash.cloneDeep(name);

View File

@ -45,12 +45,39 @@ export interface InstallOptions {
export class AddPresetError extends Error {} export class AddPresetError extends Error {}
export class PluginManager { export class PluginManager {
/**
* @internal
*/
app: Application; app: Application;
/**
* @internal
*/
collection: Collection; collection: Collection;
/**
* @internal
*/
pluginInstances = new Map<typeof Plugin, Plugin>(); pluginInstances = new Map<typeof Plugin, Plugin>();
/**
* @internal
*/
pluginAliases = new Map<string, Plugin>(); pluginAliases = new Map<string, Plugin>();
/**
* @internal
*/
server: net.Server; server: net.Server;
/**
* @internal
*/
_repository: PluginManagerRepository;
/**
* @internal
*/
constructor(public options: PluginManagerOptions) { constructor(public options: PluginManagerOptions) {
this.app = options.app; this.app = options.app;
this.app.db.registerRepositories({ this.app.db.registerRepositories({
@ -76,18 +103,22 @@ export class PluginManager {
this.app.resourcer.use(uploadMiddleware); this.app.resourcer.use(uploadMiddleware);
} }
_repository: PluginManagerRepository;
get repository() { get repository() {
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository; return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
} }
/**
* @internal
*/
static async getPackageJson(packageName: string) { static async getPackageJson(packageName: string) {
const file = await fs.promises.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json')); const file = await fs.promises.realpath(resolve(process.env.NODE_MODULES_PATH, packageName, 'package.json'));
const data = await fs.promises.readFile(file, { encoding: 'utf-8' }); const data = await fs.promises.readFile(file, { encoding: 'utf-8' });
return JSON.parse(data); return JSON.parse(data);
} }
/**
* @internal
*/
static async getPackageName(name: string) { static async getPackageName(name: string) {
const prefixes = this.getPluginPkgPrefix(); const prefixes = this.getPluginPkgPrefix();
for (const prefix of prefixes) { for (const prefix of prefixes) {
@ -100,12 +131,18 @@ export class PluginManager {
throw new Error(`${name} plugin does not exist`); throw new Error(`${name} plugin does not exist`);
} }
/**
* @internal
*/
static getPluginPkgPrefix() { static getPluginPkgPrefix() {
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split( return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
',', ',',
); );
} }
/**
* @internal
*/
static async findPackage(name: string) { static async findPackage(name: string) {
try { try {
const packageName = this.getPackageName(name); const packageName = this.getPackageName(name);
@ -130,6 +167,9 @@ export class PluginManager {
throw new Error(`No available packages found, ${name} plugin does not exist`); throw new Error(`No available packages found, ${name} plugin does not exist`);
} }
/**
* @internal
*/
static clearCache(packageName: string) { static clearCache(packageName: string) {
return; return;
const packageNamePath = packageName.replace('/', sep); const packageNamePath = packageName.replace('/', sep);
@ -140,6 +180,9 @@ export class PluginManager {
}); });
} }
/**
* @internal
*/
static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) { static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) {
if (typeof pluginName === 'string') { if (typeof pluginName === 'string') {
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName); const packageName = isPkg ? pluginName : await this.getPackageName(pluginName);
@ -296,11 +339,17 @@ export class PluginManager {
await instance.afterAdd(); await instance.afterAdd();
} }
/**
* @internal
*/
async initPlugins() { async initPlugins() {
await this.initPresetPlugins(); await this.initPresetPlugins();
await this.initOtherPlugins(); await this.initOtherPlugins();
} }
/**
* @internal
*/
async loadCommands() { async loadCommands() {
this.app.log.debug('load commands'); this.app.log.debug('load commands');
const items = await this.repository.find({ const items = await this.repository.find({
@ -619,6 +668,9 @@ export class PluginManager {
await execa('yarn', ['nocobase', 'refresh']); await execa('yarn', ['nocobase', 'refresh']);
} }
/**
* @deprecated
*/
async loadOne(plugin: Plugin) { async loadOne(plugin: Plugin) {
this.app.setMaintainingMessage(`loading plugin ${plugin.name}...`); this.app.setMaintainingMessage(`loading plugin ${plugin.name}...`);
if (plugin.state.loaded || !plugin.enabled) { if (plugin.state.loaded || !plugin.enabled) {
@ -637,6 +689,9 @@ export class PluginManager {
this.app.setMaintainingMessage(`loaded plugin ${plugin.name}`); this.app.setMaintainingMessage(`loaded plugin ${plugin.name}`);
} }
/**
* @internal
*/
async addViaCLI(urlOrName: string, options?: PluginData) { async addViaCLI(urlOrName: string, options?: PluginData) {
if (isURL(urlOrName)) { if (isURL(urlOrName)) {
await this.addByCompressedFileUrl({ await this.addByCompressedFileUrl({
@ -679,6 +734,9 @@ export class PluginManager {
await execa('yarn', ['nocobase', 'postinstall']); await execa('yarn', ['nocobase', 'postinstall']);
} }
/**
* @internal
*/
async addByNpm(options: { packageName: string; name?: string; registry: string; authToken?: string }) { async addByNpm(options: { packageName: string; name?: string; registry: string; authToken?: string }) {
let { name = '', registry, packageName, authToken } = options; let { name = '', registry, packageName, authToken } = options;
name = name.trim(); name = name.trim();
@ -693,6 +751,9 @@ export class PluginManager {
return this.addByCompressedFileUrl({ name, compressedFileUrl, registry, authToken, type: 'npm' }); return this.addByCompressedFileUrl({ name, compressedFileUrl, registry, authToken, type: 'npm' });
} }
/**
* @internal
*/
async addByFile(options: { file: string; registry?: string; authToken?: string; type?: string; name?: string }) { async addByFile(options: { file: string; registry?: string; authToken?: string; type?: string; name?: string }) {
const { file, authToken } = options; const { file, authToken } = options;
@ -708,6 +769,9 @@ export class PluginManager {
return this.add(name, { packageName }, true); return this.add(name, { packageName }, true);
} }
/**
* @internal
*/
async addByCompressedFileUrl(options: { async addByCompressedFileUrl(options: {
compressedFileUrl: string; compressedFileUrl: string;
registry?: string; registry?: string;
@ -748,6 +812,9 @@ export class PluginManager {
await this.app.upgrade(); await this.app.upgrade();
} }
/**
* @internal
*/
async upgradeByNpm(values: PluginData) { async upgradeByNpm(values: PluginData) {
const name = values.name; const name = values.name;
const plugin = this.get(name); const plugin = this.get(name);
@ -769,6 +836,9 @@ export class PluginManager {
return this.upgradeByCompressedFileUrl({ compressedFileUrl, name, version, registry, authToken }); return this.upgradeByCompressedFileUrl({ compressedFileUrl, name, version, registry, authToken });
} }
/**
* @internal
*/
async upgradeByCompressedFileUrl(options: PluginData) { async upgradeByCompressedFileUrl(options: PluginData) {
const { name, compressedFileUrl, authToken } = options; const { name, compressedFileUrl, authToken } = options;
const data = await this.repository.findOne({ filter: { name } }); const data = await this.repository.findOne({ filter: { name } });
@ -780,6 +850,9 @@ export class PluginManager {
await this.add(name, { version, packageName: data.packageName }, true, true); await this.add(name, { version, packageName: data.packageName }, true, true);
} }
/**
* @internal
*/
getNameByPackageName(packageName: string) { getNameByPackageName(packageName: string) {
const prefixes = PluginManager.getPluginPkgPrefix(); const prefixes = PluginManager.getPluginPkgPrefix();
const prefix = prefixes.find((prefix) => packageName.startsWith(prefix)); const prefix = prefixes.find((prefix) => packageName.startsWith(prefix));
@ -808,12 +881,18 @@ export class PluginManager {
); );
} }
/**
* @internal
*/
async getNpmVersionList(name: string) { async getNpmVersionList(name: string) {
const plugin = this.get(name); const plugin = this.get(name);
const npmInfo = await getNpmInfo(plugin.options.packageName, plugin.options.registry, plugin.options.authToken); const npmInfo = await getNpmInfo(plugin.options.packageName, plugin.options.registry, plugin.options.authToken);
return Object.keys(npmInfo.versions); return Object.keys(npmInfo.versions);
} }
/**
* @internal
*/
async loadPresetMigrations() { async loadPresetMigrations() {
const migrations = { const migrations = {
beforeLoad: [], beforeLoad: [],
@ -854,6 +933,9 @@ export class PluginManager {
}; };
} }
/**
* @internal
*/
async loadOtherMigrations() { async loadOtherMigrations() {
const migrations = { const migrations = {
beforeLoad: [], beforeLoad: [],
@ -897,6 +979,9 @@ export class PluginManager {
}; };
} }
/**
* @internal
*/
async loadPresetPlugins() { async loadPresetPlugins() {
await this.initPresetPlugins(); await this.initPresetPlugins();
await this.load(); await this.load();
@ -931,6 +1016,9 @@ export class PluginManager {
}); });
} }
/**
* @internal
*/
async initOtherPlugins() { async initOtherPlugins() {
if (this['_initOtherPlugins']) { if (this['_initOtherPlugins']) {
return; return;
@ -939,6 +1027,9 @@ export class PluginManager {
this['_initOtherPlugins'] = true; this['_initOtherPlugins'] = true;
} }
/**
* @internal
*/
async initPresetPlugins() { async initPresetPlugins() {
if (this['_initPresetPlugins']) { if (this['_initPresetPlugins']) {
return; return;

View File

@ -33,9 +33,22 @@ export interface PluginOptions {
export abstract class Plugin<O = any> implements PluginInterface { export abstract class Plugin<O = any> implements PluginInterface {
options: any; options: any;
app: Application; app: Application;
/**
* @deprecated
*/
model: Model; model: Model;
/**
* @internal
*/
state: any = {}; state: any = {};
/**
* @internal
*/
private _sourceDir: string;
constructor(app: Application, options?: any) { constructor(app: Application, options?: any) {
this.app = app; this.app = app;
this.setOptions(options); this.setOptions(options);
@ -80,10 +93,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
return this.options.isPreset; return this.options.isPreset;
} }
setOptions(options: any) {
this.options = options || {};
}
getName() { getName() {
return (this.options as any).name; return (this.options as any).name;
} }
@ -92,64 +101,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
return this.app.createLogger(options); return this.app.createLogger(options);
} }
protected _sourceDir: string;
protected async getSourceDir() {
if (this._sourceDir) {
return this._sourceDir;
}
if (await this.isDev()) {
return (this._sourceDir = 'src');
}
if (basename(__dirname) === 'src') {
return (this._sourceDir = 'src');
}
return (this._sourceDir = this.isPreset ? 'lib' : 'dist');
}
async loadCommands() {
const extensions = ['js', 'ts'];
const directory = resolve(
process.env.NODE_MODULES_PATH,
this.options.packageName,
await this.getSourceDir(),
'server/commands',
);
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten, {
ignore: ['**/*.d.ts'],
});
for (const file of files) {
let filename = basename(file);
filename = filename.substring(0, filename.lastIndexOf('.')) || filename;
const callback = await importModule(file);
callback(this.app);
}
if (files.length) {
this.app.log.debug(`load commands [${this.name}]`);
}
}
async loadMigrations() {
this.app.log.debug(`load plugin migrations [${this.name}]`);
if (!this.options.packageName) {
return { beforeLoad: [], afterSync: [], afterLoad: [] };
}
const directory = resolve(
process.env.NODE_MODULES_PATH,
this.options.packageName,
await this.getSourceDir(),
'server/migrations',
);
return await this.app.loadMigrations({
directory,
namespace: this.options.packageName,
context: {
plugin: this,
},
});
}
afterAdd() {} afterAdd() {}
beforeLoad() {} beforeLoad() {}
@ -172,13 +123,86 @@ export abstract class Plugin<O = any> implements PluginInterface {
async afterRemove() {} async afterRemove() {}
async importCollections(collectionsPath: string) { /**
// await this.db.import({ * @deprecated
// directory: collectionsPath, */
// from: `plugin:${this.getName()}`, async importCollections(collectionsPath: string) {}
// });
/**
* @internal
*/
setOptions(options: any) {
this.options = options || {};
} }
/**
* @internal
*/
protected async getSourceDir() {
if (this._sourceDir) {
return this._sourceDir;
}
if (await this.isDev()) {
return (this._sourceDir = 'src');
}
if (basename(__dirname) === 'src') {
return (this._sourceDir = 'src');
}
return (this._sourceDir = this.isPreset ? 'lib' : 'dist');
}
/**
* @internal
*/
async loadCommands() {
const extensions = ['js', 'ts'];
const directory = resolve(
process.env.NODE_MODULES_PATH,
this.options.packageName,
await this.getSourceDir(),
'server/commands',
);
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten, {
ignore: ['**/*.d.ts'],
});
for (const file of files) {
let filename = basename(file);
filename = filename.substring(0, filename.lastIndexOf('.')) || filename;
const callback = await importModule(file);
callback(this.app);
}
if (files.length) {
this.app.log.debug(`load commands [${this.name}]`);
}
}
/**
* @internal
*/
async loadMigrations() {
this.app.log.debug(`load plugin migrations [${this.name}]`);
if (!this.options.packageName) {
return { beforeLoad: [], afterSync: [], afterLoad: [] };
}
const directory = resolve(
process.env.NODE_MODULES_PATH,
this.options.packageName,
await this.getSourceDir(),
'server/migrations',
);
return await this.app.loadMigrations({
directory,
namespace: this.options.packageName,
context: {
plugin: this,
},
});
}
/**
* @internal
*/
async loadCollections() { async loadCollections() {
if (!this.options.packageName) { if (!this.options.packageName) {
return; return;
@ -197,6 +221,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
} }
} }
/**
* @deprecated
*/
requiredPlugins() { requiredPlugins() {
return []; return [];
} }
@ -205,6 +232,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
return this.app.i18n.t(text, { ns: this.options['packageName'], ...(options as any) }); return this.app.i18n.t(text, { ns: this.options['packageName'], ...(options as any) });
} }
/**
* @internal
*/
protected async isDev() { protected async isDev() {
if (!this.options.packageName) { if (!this.options.packageName) {
return false; return false;
@ -218,6 +248,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
return false; return false;
} }
/**
* @experimental
*/
async toJSON(options: any = {}) { async toJSON(options: any = {}) {
const { locale = 'en-US' } = options; const { locale = 'en-US' } = options;
const { name, packageName, packageJson } = this.options; const { name, packageName, packageJson } = this.options;

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/telemetry", "name": "@nocobase/telemetry",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"description": "nocobase telemetry library", "description": "nocobase telemetry library",
"license": "Apache-2.0", "license": "Apache-2.0",
"main": "./lib/index.js", "main": "./lib/index.js",
@ -11,7 +11,7 @@
"directory": "packages/telemetry" "directory": "packages/telemetry"
}, },
"dependencies": { "dependencies": {
"@nocobase/utils": "0.20.0-alpha.16", "@nocobase/utils": "0.20.0-alpha.17",
"@opentelemetry/api": "^1.7.0", "@opentelemetry/api": "^1.7.0",
"@opentelemetry/instrumentation": "^0.46.0", "@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/resources": "^1.19.0", "@opentelemetry/resources": "^1.19.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@nocobase/test", "name": "@nocobase/test",
"version": "0.20.0-alpha.16", "version": "0.20.0-alpha.17",
"main": "lib/index.js", "main": "lib/index.js",
"module": "./src/index.ts", "module": "./src/index.ts",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
@ -40,9 +40,10 @@
}, },
"dependencies": { "dependencies": {
"@faker-js/faker": "8.1.0", "@faker-js/faker": "8.1.0",
"@nocobase/server": "0.20.0-alpha.16", "@nocobase/server": "0.20.0-alpha.17",
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
@ -56,7 +57,7 @@
"sqlite3": "^5.0.8", "sqlite3": "^5.0.8",
"supertest": "^6.1.6", "supertest": "^6.1.6",
"vite": "^5.0.0", "vite": "^5.0.0",
"vitest": "^1.0.0", "vitest": "^1.4.0",
"vitest-dom": "^0.1.1", "vitest-dom": "^0.1.1",
"ws": "^8.13.0" "ws": "^8.13.0"
}, },

View File

@ -1,4 +1,5 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
export { renderHook } from '@testing-library/react-hooks';
function customRender(ui: React.ReactElement, options = {}) { function customRender(ui: React.ReactElement, options = {}) {
return render(ui, { return render(ui, {

View File

@ -21,7 +21,7 @@ export const defineConfig = (config?: PlaywrightTestConfig) => {
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
// Retry on CI only. // Retry on CI only.
retries: process.env.CI ? 2 : 0, retries: 2,
// Opt out of parallel tests on CI. // Opt out of parallel tests on CI.
// workers: process.env.CI ? 1 : undefined, // workers: process.env.CI ? 1 : undefined,
@ -52,7 +52,14 @@ export const defineConfig = (config?: PlaywrightTestConfig) => {
}, },
{ {
name: 'chromium', name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: process.env.PLAYWRIGHT_AUTH_FILE }, use: {
...devices['Desktop Chrome'],
storageState: process.env.PLAYWRIGHT_AUTH_FILE,
contextOptions: {
// chromium-specific permissions
permissions: ['clipboard-read', 'clipboard-write'],
},
},
dependencies: ['authSetup'], dependencies: ['authSetup'],
}, },
], ],

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