Merge remote-tracking branch 'upstream/main' into merge_from_upstream
This commit is contained in:
commit
f364b40e79
4
.github/workflows/nocobase-test-backend.yml
vendored
4
.github/workflows/nocobase-test-backend.yml
vendored
@ -13,6 +13,8 @@ on:
|
||||
- 'packages/core/acl/**'
|
||||
- 'packages/core/actions/**'
|
||||
- 'packages/core/database/**'
|
||||
- 'packages/core/resourcer/**'
|
||||
- 'packages/core/data-source-manager/**'
|
||||
- 'packages/core/server/**'
|
||||
- 'packages/core/utils/**'
|
||||
- 'packages/plugins/**/src/server/**'
|
||||
@ -23,6 +25,8 @@ on:
|
||||
- 'packages/core/acl/**'
|
||||
- 'packages/core/actions/**'
|
||||
- 'packages/core/database/**'
|
||||
- 'packages/core/resourcer/**'
|
||||
- 'packages/core/data-source-manager/**'
|
||||
- 'packages/core/server/**'
|
||||
- 'packages/core/utils/**'
|
||||
- 'packages/plugins/**/src/server/**'
|
||||
|
34
CHANGELOG.md
34
CHANGELOG.md
@ -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).
|
||||
|
||||
## [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
|
||||
|
||||
### Merged
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/acl",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/resourcer": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"minimatch": "^5.1.1"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -45,14 +45,40 @@ interface CanArgs {
|
||||
}
|
||||
|
||||
export class ACL extends EventEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public availableStrategy = new Map<string, ACLAvailableStrategy>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public allowManager = new AllowManager(this);
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public snippetManager = new SnippetManager();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
roles = new Map<string, ACLRole>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
actionAlias = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
configResources: string[] = [];
|
||||
|
||||
protected availableActions = new Map<string, ACLAvailableAction>();
|
||||
|
||||
protected fixedParamsManager = new FixedParamsManager();
|
||||
|
||||
protected middlewares: Toposort<any>;
|
||||
|
||||
constructor() {
|
||||
@ -114,14 +140,23 @@ export class ACL extends EventEmitter {
|
||||
return this.roles.delete(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
registerConfigResources(names: string[]) {
|
||||
names.forEach((name) => this.registerConfigResource(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
registerConfigResource(name: string) {
|
||||
this.configResources.push(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
isConfigResource(name: string) {
|
||||
return this.configResources.includes(name);
|
||||
}
|
||||
@ -227,6 +262,9 @@ export class ACL extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public resolveActionAlias(action: string) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
|
||||
if (!Array.isArray(actionNames)) {
|
||||
actionNames = [actionNames];
|
||||
@ -252,6 +293,9 @@ export class ACL extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async parseJsonTemplate(json: any, ctx: any) {
|
||||
if (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) {
|
||||
const roleName = ctx.state.currentRole || 'anonymous';
|
||||
const { resourceName, actionName } = ctx.action;
|
||||
@ -322,6 +369,9 @@ export class ACL extends EventEmitter {
|
||||
this.snippetManager.register(snippet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
filterParams(ctx, resourceName, params) {
|
||||
if (params?.filter?.createdById) {
|
||||
const collection = ctx.db.getCollection(resourceName);
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@nocobase/actions",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/cache": "0.20.0-alpha.16",
|
||||
"@nocobase/database": "0.20.0-alpha.16",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.16"
|
||||
"@nocobase/cache": "0.20.0-alpha.17",
|
||||
"@nocobase/database": "0.20.0-alpha.17",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.17"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@nocobase/app",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/database": "0.20.0-alpha.16",
|
||||
"@nocobase/preset-nocobase": "0.20.0-alpha.16",
|
||||
"@nocobase/server": "0.20.0-alpha.16"
|
||||
"@nocobase/database": "0.20.0-alpha.17",
|
||||
"@nocobase/preset-nocobase": "0.20.0-alpha.17",
|
||||
"@nocobase/server": "0.20.0-alpha.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/client": "0.20.0-alpha.16"
|
||||
"@nocobase/client": "0.20.0-alpha.17"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/auth",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "0.20.0-alpha.16",
|
||||
"@nocobase/cache": "0.20.0-alpha.16",
|
||||
"@nocobase/database": "0.20.0-alpha.16",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/actions": "0.20.0-alpha.17",
|
||||
"@nocobase/cache": "0.20.0-alpha.17",
|
||||
"@nocobase/database": "0.20.0-alpha.17",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/build",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "Library build tool based on rollup.",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
|
@ -14,16 +14,22 @@ import { buildClient } from './buildClient';
|
||||
import { buildCjs } from './buildCjs';
|
||||
import { buildPlugin } from './buildPlugin';
|
||||
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 { Package } from '@lerna/package';
|
||||
import { tarPlugin } from './tarPlugin'
|
||||
|
||||
const BUILD_ERROR = 'build-error';
|
||||
|
||||
export async function build(pkgs: string[]) {
|
||||
const isDev = process.argv.includes('--development');
|
||||
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) {
|
||||
let msg = '';
|
||||
if (pkgs.length) {
|
||||
@ -59,6 +65,7 @@ export async function build(pkgs: string[]) {
|
||||
APP_ROOT: path.join(CORE_APP, 'client'),
|
||||
});
|
||||
}
|
||||
writeToCache(BUILD_ERROR, {});
|
||||
}
|
||||
|
||||
export async function buildPackages(
|
||||
@ -67,6 +74,7 @@ export async function buildPackages(
|
||||
doBuildPackage: (cwd: string, userConfig: UserConfig, sourcemap: boolean, log?: PkgLog) => Promise<any>,
|
||||
) {
|
||||
for await (const pkg of packages) {
|
||||
writeToCache(BUILD_ERROR, { pkg: pkg.name })
|
||||
await buildPackage(pkg, targetDir, doBuildPackage);
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ export const EsbuildSupportExts = [
|
||||
'.data',
|
||||
];
|
||||
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 PLUGINS_DIR = ['plugins', 'samples', 'pro-plugins']
|
||||
.concat((process.env.PLUGINS_DIRS || '').split(','))
|
||||
|
@ -10,6 +10,7 @@ import { toUnixPath } from './utils';
|
||||
* 获取构建包的绝对路径,支持项目路径和 npm 两种形式
|
||||
* @example
|
||||
* 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
|
||||
*/
|
||||
function getPackagesPath(pkgs: string[]) {
|
||||
@ -24,7 +25,6 @@ function getPackagesPath(pkgs: string[]) {
|
||||
return allPackageJson
|
||||
.map(toUnixPath).map(item => path.dirname(item));
|
||||
}
|
||||
|
||||
const allPackageInfo = allPackageJson
|
||||
.map(packageJsonPath => ({ name: require(packageJsonPath).name, path: path.dirname(toUnixPath(packageJsonPath)) }))
|
||||
.reduce((acc, cur) => {
|
||||
@ -37,7 +37,9 @@ function getPackagesPath(pkgs: string[]) {
|
||||
const relativePaths = pkgNames.length ? pkgs.filter(item => !pkgNames.includes(item)) : pkgs;
|
||||
const pkgPaths = pkgs.map(item => allPackageInfo[item])
|
||||
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[]) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import { Options as TsupConfig } from 'tsup'
|
||||
import { InlineConfig as ViteConfig } from 'vite'
|
||||
import { register } from 'esbuild-register/dist/node';
|
||||
import { NODE_MODULES } from '../constant';
|
||||
|
||||
let previousColor = '';
|
||||
function randomColor() {
|
||||
@ -79,3 +80,18 @@ export function getUserConfig(cwd: string) {
|
||||
}
|
||||
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 {};
|
||||
}
|
||||
|
2
packages/core/cache/package.json
vendored
2
packages/core/cache/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cache",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/cli",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./src/index.js",
|
||||
@ -8,7 +8,7 @@
|
||||
"nocobase": "./bin/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/app": "0.20.0-alpha.16",
|
||||
"@nocobase/app": "0.20.0-alpha.17",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@umijs/utils": "3.5.20",
|
||||
"chalk": "^4.1.1",
|
||||
@ -25,7 +25,7 @@
|
||||
"tsx": "^4.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/devtools": "0.20.0-alpha.16"
|
||||
"@nocobase/devtools": "0.20.0-alpha.17"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -13,6 +13,7 @@ module.exports = (cli) => {
|
||||
.argument('[packages...]')
|
||||
.option('-v, --version', 'print version')
|
||||
.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('-s, --sourcemap', 'generate sourcemap')
|
||||
.option('--no-dts', 'not generate dts')
|
||||
@ -24,12 +25,14 @@ module.exports = (cli) => {
|
||||
});
|
||||
if (options.watch) return;
|
||||
}
|
||||
process.env['VITE_CJS_IGNORE_WARNING'] = 'true';
|
||||
|
||||
await run('nocobase-build', [
|
||||
...pkgs,
|
||||
options.version ? '--version' : '',
|
||||
!options.dts ? '--no-dts' : '',
|
||||
options.sourcemap ? '--sourcemap' : '',
|
||||
options.retry ? '--retry' : '',
|
||||
]);
|
||||
buildIndexHtml(true);
|
||||
});
|
||||
|
@ -243,6 +243,12 @@ module.exports = (cli) => {
|
||||
.option('--stop-on-error')
|
||||
.option('--build')
|
||||
.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) => {
|
||||
process.env.__E2E__ = true;
|
||||
if (options.build) {
|
||||
|
@ -60,7 +60,8 @@ exports.pTest = async (options) => {
|
||||
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(),
|
||||
});
|
||||
|
||||
|
@ -19,6 +19,7 @@ function addTestCommand(name, cli) {
|
||||
.arguments('[paths...]')
|
||||
.allowUnknownOption()
|
||||
.action(async (paths, opts) => {
|
||||
process.argv.push('--disable-console-intercept');
|
||||
if (name === 'test:server') {
|
||||
process.env.TEST_ENV = 'server-side';
|
||||
} else if (name === 'test:client') {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/client",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
"module": "es/index.mjs",
|
||||
@ -26,9 +26,9 @@
|
||||
"@formily/reactive-react": "^2.2.27",
|
||||
"@formily/shared": "^2.2.27",
|
||||
"@formily/validator": "^2.2.27",
|
||||
"@nocobase/evaluators": "0.20.0-alpha.16",
|
||||
"@nocobase/sdk": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/evaluators": "0.20.0-alpha.17",
|
||||
"@nocobase/sdk": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"ahooks": "^3.7.2",
|
||||
"antd": "^5.12.8",
|
||||
"antd-style": "3.4.5",
|
||||
|
@ -11,7 +11,7 @@ export const ADMIN_SETTINGS_PATH = '/admin/settings/';
|
||||
export const SNIPPET_PREFIX = 'pm.';
|
||||
|
||||
export interface PluginSettingOptions {
|
||||
title: string | React.ReactElement;
|
||||
title: any;
|
||||
/**
|
||||
* @default Outlet
|
||||
*/
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
const HelloComponent = withDynamicSchemaProps((props: any) => (
|
||||
@ -66,7 +66,7 @@ describe('withDynamicSchemaProps', () => {
|
||||
|
||||
const Demo = withTestDemo(schema, scopes);
|
||||
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', () => {
|
||||
@ -101,7 +101,7 @@ describe('withDynamicSchemaProps', () => {
|
||||
|
||||
const Demo = withTestDemo(schema, scopes);
|
||||
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', () => {
|
||||
@ -130,8 +130,8 @@ describe('withDynamicSchemaProps', () => {
|
||||
|
||||
const Demo = withTestDemo(schema, scopes);
|
||||
const { getByTestId } = render(<Demo />);
|
||||
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ a: 'a', b: 'b' }));
|
||||
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ c: 'c', d: 'd' }));
|
||||
expect(getByTestId('decorator')).toHaveTextContent(JSON.stringify({ b: 'b', a: 'a' }));
|
||||
expect(getByTestId('component')).toHaveTextContent(JSON.stringify({ d: 'd', c: 'c' }));
|
||||
});
|
||||
|
||||
test('no register scope', () => {
|
||||
@ -142,4 +142,23 @@ describe('withDynamicSchemaProps', () => {
|
||||
const { getByTestId } = render(<Demo />);
|
||||
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' }));
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useExpressionScope } from '@formily/react';
|
||||
import { merge, omit } from 'lodash';
|
||||
import React, { ComponentType, useMemo } from 'react';
|
||||
import { useDesignable, useSchemaComponentContext } from '../../schema-component';
|
||||
import { useDesignable } from '../../schema-component';
|
||||
|
||||
const useDefaultSchemaProps = () => undefined;
|
||||
|
||||
@ -8,11 +9,11 @@ interface WithSchemaHookOptions {
|
||||
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 ComponentWithProps: ComponentType<T> = (props) => {
|
||||
const { dn, findComponent } = useDesignable();
|
||||
const { scope } = useSchemaComponentContext();
|
||||
const scope = useExpressionScope();
|
||||
const useComponentPropsStr = useMemo(() => {
|
||||
const xComponent = dn.getSchemaAttribute('x-component');
|
||||
const xDecorator = dn.getSchemaAttribute('x-decorator');
|
||||
@ -40,7 +41,7 @@ export function withDynamicSchemaProps<T = any>(Component: ComponentType<T>, opt
|
||||
const schemaProps = useSchemaProps(props);
|
||||
|
||||
const memoProps = useMemo(() => {
|
||||
return merge(omit(schemaProps, 'children'), omit(props, 'children'));
|
||||
return merge(omit(props, 'children'), omit(schemaProps, 'children'));
|
||||
}, [schemaProps, props]);
|
||||
|
||||
return <Component {...memoProps}>{props.children}</Component>;
|
||||
|
@ -59,31 +59,6 @@ export const useBlockResource = () => {
|
||||
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) => {
|
||||
const { collection } = props;
|
||||
return collection ? (
|
||||
@ -218,6 +193,22 @@ export const useBlockContext = () => {
|
||||
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
|
||||
*/
|
||||
@ -236,8 +227,11 @@ export const BlockProvider = (props: {
|
||||
useParams?: any;
|
||||
}) => {
|
||||
const { name, dataSource, association, useParams, parentRecord } = props;
|
||||
const sourceId = useDataBlockSourceId({ association });
|
||||
const sourceId = useCompatDataBlockSourceId(props);
|
||||
|
||||
// 新版(1.0)已弃用 useParams,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const paramsFromHook = useParams?.();
|
||||
|
||||
const { getAssociationAppends } = useAssociationNames(dataSource);
|
||||
const { appends, updateAssociationValues } = getAssociationAppends();
|
||||
const params = useMemo(() => {
|
||||
@ -330,7 +324,9 @@ export const useParamsFromRecord = () => {
|
||||
const { fields } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
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
|
||||
.filter((v) => {
|
||||
return ['boolean', 'date', 'integer', 'radio', 'sort', 'string', 'time', 'uid', 'uuid'].includes(v.type);
|
||||
|
@ -10,11 +10,13 @@ import { DetailsBlockProvider, useDetailsBlockProps } from './DetailsBlockProvid
|
||||
import { FilterFormBlockProvider } from './FilterFormBlockProvider';
|
||||
import { FormBlockProvider, useFormBlockProps } from './FormBlockProvider';
|
||||
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 { TableSelectorProvider, useTableSelectorProps } from './TableSelectorProvider';
|
||||
import * as bp from './hooks';
|
||||
import { BlockSchemaToolbar } from '../modules/blocks/BlockSchemaToolbar';
|
||||
import { useTableBlockDecoratorProps } from '../modules/blocks/data-blocks/table/hooks/useTableBlockDecoratorProps';
|
||||
|
||||
// TODO: delete this, replaced by `BlockSchemaComponentPlugin`
|
||||
export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
@ -41,6 +43,7 @@ export const BlockSchemaComponentProvider: React.FC = (props) => {
|
||||
useTableFieldProps,
|
||||
useTableBlockProps,
|
||||
useTableSelectorProps,
|
||||
useTableBlockDecoratorProps,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
@ -84,6 +87,7 @@ export class BlockSchemaComponentPlugin extends Plugin {
|
||||
useTableFieldProps,
|
||||
useTableBlockProps,
|
||||
useTableSelectorProps,
|
||||
useTableBlockDecoratorProps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { ArrayField, createForm } from '@formily/core';
|
||||
import { createForm } from '@formily/core';
|
||||
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 { useFilterBlock } from '../filter-provider/FilterProvider';
|
||||
import { mergeFilter } from '../filter-provider/utils';
|
||||
import { FixedBlockWrapper, SchemaComponentOptions, removeNullCondition } from '../schema-component';
|
||||
import { FixedBlockWrapper, SchemaComponentOptions } from '../schema-component';
|
||||
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>({});
|
||||
TableBlockContext.displayName = 'TableBlockContext';
|
||||
@ -74,16 +74,37 @@ const InternalTableBlockProvider = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const TableBlockProvider = (props) => {
|
||||
const resourceName = props.resource;
|
||||
const params = useMemo(() => ({ ...props.params }), [props.params]);
|
||||
/**
|
||||
* 用于兼容旧版本的 schema,当不需要兼容时可直接移除该方法
|
||||
* @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 { getCollection, getCollectionField } = useCollectionManager_deprecated(props.dataSource);
|
||||
const collection = getCollection(props.collection, props.dataSource);
|
||||
const { treeTable, dragSortBy } = fieldSchema?.['x-decorator-props'] || {};
|
||||
if (props.dragSort && dragSortBy) {
|
||||
params['sort'] = dragSortBy;
|
||||
}
|
||||
const { treeTable } = fieldSchema?.['x-decorator-props'] || {};
|
||||
const params = useTableBlockParamsCompat(props);
|
||||
|
||||
let childrenColumnName = 'children';
|
||||
if (collection?.tree && treeTable !== false) {
|
||||
if (resourceName?.includes('.')) {
|
||||
@ -101,140 +122,18 @@ export const TableBlockProvider = (props) => {
|
||||
}
|
||||
}
|
||||
const form = useMemo(() => createForm(), [treeTable]);
|
||||
const { filter: parsedFilter } = useParsedFilter({
|
||||
filterOption: params?.filter,
|
||||
});
|
||||
const paramsWithFilter = useMemo(() => {
|
||||
return {
|
||||
...params,
|
||||
filter: parsedFilter,
|
||||
};
|
||||
}, [parsedFilter, params]);
|
||||
|
||||
return (
|
||||
<SchemaComponentOptions scope={{ treeTable }}>
|
||||
<FormContext.Provider value={form}>
|
||||
<BlockProvider name={props.name || 'table'} {...props} params={paramsWithFilter} runWhenParamsChanged>
|
||||
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={paramsWithFilter} />
|
||||
<BlockProvider name={props.name || 'table'} {...props} params={params} runWhenParamsChanged>
|
||||
<InternalTableBlockProvider {...props} childrenColumnName={childrenColumnName} params={params} />
|
||||
</BlockProvider>
|
||||
</FormContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const useTableBlockContext = () => {
|
||||
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);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { useCollection, useCollectionManager, useCollectionParentRecordData, useCollectionRecordData } from '../..';
|
||||
import {
|
||||
InheritanceCollectionMixin,
|
||||
useCollection,
|
||||
useCollectionManager,
|
||||
useCollectionParentRecordData,
|
||||
useCollectionRecordData,
|
||||
} from '../..';
|
||||
|
||||
/**
|
||||
* 注意:这里有一个需要更改 schema 才能解决的问题,就是在获取 sourceId 的时候无法确定(在关系字段和当前表同表时)
|
||||
@ -12,14 +17,17 @@ export const useDataBlockSourceId = ({ association }: { association: string }) =
|
||||
const recordData = useCollectionRecordData();
|
||||
const parentRecordData = useCollectionParentRecordData();
|
||||
const cm = useCollectionManager();
|
||||
const collectionOutsideBlock = useCollection();
|
||||
const collectionOutsideBlock = useCollection<InheritanceCollectionMixin>();
|
||||
|
||||
if (!association) return;
|
||||
|
||||
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?.[
|
||||
associationField.sourceKey ||
|
||||
associationCollection.filterTargetKey ||
|
||||
|
@ -193,6 +193,10 @@ export const PresetFields = observer(
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
selectedRowKeys,
|
||||
getCheckboxProps: (record) => ({
|
||||
disabled: form.values.template === 'file', // Column configuration not to be checked
|
||||
name: record.name,
|
||||
}),
|
||||
onChange: (_, selectedRows) => {
|
||||
const fields = getDefaultCollectionFields(selectedRows, form.values);
|
||||
const config = {
|
||||
|
@ -11,6 +11,7 @@ export interface DataSourceOptions {
|
||||
collections?: CollectionOptions[];
|
||||
errorMessage?: string;
|
||||
status?: 'loaded' | 'loading-failed' | 'loading' | 'reloading';
|
||||
isDBInstance?: boolean;
|
||||
}
|
||||
|
||||
export type DataSourceFactory = new (options: DataSourceOptions, dataSourceManager: DataSourceManager) => DataSource;
|
||||
|
@ -910,5 +910,14 @@
|
||||
"Second": "秒",
|
||||
"Unix Timestamp": "Unix 时间戳",
|
||||
"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":"常规"
|
||||
}
|
||||
|
@ -177,7 +177,8 @@ export const multiDataDetailsBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Details',
|
||||
collectionName: name,
|
||||
|
@ -16,7 +16,8 @@ export const singleDataDetailsBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
insertAdjacentPosition: 'beforeEnd',
|
||||
componentName: 'ReadPrettyFormItem',
|
||||
|
@ -51,7 +51,8 @@ export const creationFormBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
collectionName: name,
|
||||
@ -117,7 +118,8 @@ export const createFormBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
collectionName: name,
|
||||
|
@ -50,7 +50,8 @@ export const editFormBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
collectionName: name,
|
||||
|
@ -257,7 +257,8 @@ export const gridCardBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'GridCard',
|
||||
|
@ -196,7 +196,8 @@ export const listBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'List',
|
||||
|
@ -2,9 +2,9 @@ import { TableOutlined } from '@ant-design/icons';
|
||||
import { useSchemaInitializer, useSchemaInitializerItem } from '../../../../application/schema-initializer/context';
|
||||
import { useCollectionManager_deprecated } from '../../../../collection-manager/hooks/useCollectionManager_deprecated';
|
||||
import { DataBlockInitializer } from '../../../../schema-initializer/items/DataBlockInitializer';
|
||||
import { createTableBlockSchema } from '../../../../schema-initializer/utils';
|
||||
import React from 'react';
|
||||
import { Collection, CollectionFieldOptions } from '../../../../data-source/collection/Collection';
|
||||
import { createTableBlockUISchema } from './createTableBlockUISchema';
|
||||
|
||||
export const TableBlockInitializer = ({
|
||||
filterCollections,
|
||||
@ -42,8 +42,8 @@ export const TableBlockInitializer = ({
|
||||
}
|
||||
|
||||
const collection = getCollection(item.name, item.dataSource);
|
||||
const schema = createTableBlockSchema({
|
||||
collection: item.name,
|
||||
const schema = createTableBlockUISchema({
|
||||
collectionName: item.name,
|
||||
dataSource: item.dataSource,
|
||||
rowKey: collection.filterTargetKey || 'id',
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createBlockInPage, expect, oneEmptyTable, test } from '@nocobase/test/e2e';
|
||||
import { T3686 } from './templatesOfBug';
|
||||
|
||||
test.describe('where table block can be added', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { PageConfig } from '@nocobase/test/e2e';
|
||||
|
||||
export const T2183 = {
|
||||
pageSchema: {
|
||||
_isJSONSchemaObject: true,
|
||||
@ -670,3 +672,260 @@ export const T2187 = {
|
||||
'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,
|
||||
},
|
||||
};
|
||||
|
@ -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",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
@ -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: '|',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
@ -5,3 +5,5 @@ export * from './TableColumnSchemaToolbar';
|
||||
export * from './tableBlockSettings';
|
||||
export * from './tableColumnSettings';
|
||||
export * from './TableColumnInitializers';
|
||||
export * from './createTableBlockUISchema';
|
||||
export * from './hooks/useTableBlockDecoratorProps';
|
||||
|
@ -359,7 +359,8 @@ export const tableBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Table',
|
||||
collectionName: name,
|
||||
|
@ -22,7 +22,8 @@ export const filterCollapseBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return {
|
||||
componentName: 'FilterCollapse',
|
||||
|
@ -23,7 +23,8 @@ export const filterFormBlockSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FilterFormItem',
|
||||
collectionName: name,
|
||||
|
@ -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;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
@ -92,7 +92,7 @@ const LocalPlugins = () => {
|
||||
const keyWordsfilterList = useMemo(() => {
|
||||
const list = keyWordlists.map((i) => {
|
||||
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 {
|
||||
key: i,
|
||||
list: result,
|
||||
|
@ -11,7 +11,11 @@ import { usePlugin } from '../../../application/hooks';
|
||||
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||
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 { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||
@ -28,6 +32,7 @@ import {
|
||||
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||
import { useLinkageAction } from './hooks';
|
||||
import { requestSettingsSchema } from './utils';
|
||||
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
||||
|
||||
const MenuGroup = (props) => {
|
||||
return props.children;
|
||||
@ -327,7 +332,8 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
const { setValuesIn } = useForm();
|
||||
const baseCollection = useCollection_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 workflowPlugin = usePlugin('workflow') as any;
|
||||
@ -351,11 +357,11 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
const path = paths[i];
|
||||
const associationField = collection.fields.find((f) => f.name === path);
|
||||
if (associationField) {
|
||||
collection = getCollection(associationField.target);
|
||||
collection = getCollection(associationField.target, dataSourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
setWorkflowCollection(collection.name);
|
||||
setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name));
|
||||
setValuesIn(`group[${index}].workflowKey`, null);
|
||||
});
|
||||
});
|
||||
@ -375,6 +381,7 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<DataSourceProvider dataSource="main">
|
||||
<RemoteSelect
|
||||
manual={false}
|
||||
placeholder={t('Select workflow', { ns: 'workflow' })}
|
||||
@ -407,6 +414,7 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</DataSourceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -414,7 +422,7 @@ export function WorkflowConfig() {
|
||||
const { dn } = useDesignable();
|
||||
const { t } = useTranslation();
|
||||
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'.
|
||||
const formBlock = useFormBlockContext();
|
||||
const actionType = formBlock?.type || fieldSchema['x-action'];
|
||||
@ -483,7 +491,9 @@ export function WorkflowConfig() {
|
||||
'x-component-props': {
|
||||
placeholder: t('Select context', { ns: 'workflow' }),
|
||||
popupMatchSelectWidth: false,
|
||||
collection,
|
||||
collection: `${
|
||||
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
|
||||
}${collection.name}`,
|
||||
filter: '{{ fieldFilter }}',
|
||||
rootOption: {
|
||||
label: t('Full form data', { ns: 'workflow' }),
|
||||
|
@ -3,7 +3,12 @@ import { Tag, TreeSelect } from 'antd';
|
||||
import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated, useCompile } from '../../..';
|
||||
import {
|
||||
CollectionFieldOptions_deprecated,
|
||||
parseCollectionName,
|
||||
useCollectionManager_deprecated,
|
||||
useCompile,
|
||||
} from '../../..';
|
||||
|
||||
export type AppendsTreeSelectProps = {
|
||||
value: string[] | string;
|
||||
@ -27,7 +32,7 @@ function usePropsCollection({ collection }) {
|
||||
|
||||
type CallScope = {
|
||||
compile?(value: string): string;
|
||||
getCollectionFields?(name: any): CollectionFieldOptions_deprecated[];
|
||||
getCollectionFields?(name: any, dataSource?: string): CollectionFieldOptions_deprecated[];
|
||||
filter(field): boolean;
|
||||
};
|
||||
|
||||
@ -52,7 +57,8 @@ function trueFilter(field) {
|
||||
}
|
||||
|
||||
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);
|
||||
return fields.filter(this.filter).map((field) => {
|
||||
const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name;
|
||||
@ -84,11 +90,12 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
||||
loadData: propsLoadData,
|
||||
...restProps
|
||||
} = props;
|
||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
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 value: string | DefaultOptionType[] = useMemo(() => {
|
||||
if (props.multiple) {
|
||||
@ -111,7 +118,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
||||
},
|
||||
[propsLoadData],
|
||||
);
|
||||
|
||||
// NOTE:
|
||||
useEffect(() => {
|
||||
const parentNode = rootOption
|
||||
? {
|
||||
@ -123,17 +130,19 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
||||
isLeaf: false,
|
||||
}
|
||||
: null;
|
||||
const treeData =
|
||||
const tData =
|
||||
propsLoadData === null
|
||||
? []
|
||||
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, baseCollection, parentNode);
|
||||
const map = treeData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
|
||||
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode);
|
||||
|
||||
const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
|
||||
if (parentNode) {
|
||||
map[parentNode.value] = parentNode;
|
||||
}
|
||||
setOptionsMap(map);
|
||||
}, [collection, baseCollection, rootOption, filter, propsLoadData]);
|
||||
}, [collectionString, rootOption, filter, propsLoadData]);
|
||||
|
||||
// NOTE: preload options in value
|
||||
useEffect(() => {
|
||||
const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[];
|
||||
if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) {
|
||||
|
@ -18,7 +18,8 @@ export const AssociationFilterBlockDesigner = () => {
|
||||
const template = useSchemaTemplate();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { t } = useTranslation();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
|
||||
return (
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
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 { useTableBlockContext, useTableSelectorContext } from '../../../block-provider';
|
||||
import { Icon } from '../../../icon';
|
||||
@ -46,7 +47,7 @@ const actionDesignerCss = css`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExpandAction = (props) => {
|
||||
const InternalExpandAction = (props, ref) => {
|
||||
const schema = useFieldSchema();
|
||||
const ctxSelector = useTableSelectorContext();
|
||||
const ctxBlock = useTableBlockContext();
|
||||
@ -54,8 +55,11 @@ export const ExpandAction = (props) => {
|
||||
const ctx = isTableSelector ? ctxSelector : ctxBlock;
|
||||
const { titleExpand, titleCollapse, iconExpand, iconCollapse } = schema['x-component-props'] || {};
|
||||
const compile = useCompile();
|
||||
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||
const buttonRef = composeRef(ref, internalRef);
|
||||
return (
|
||||
<div className={actionDesignerCss}>
|
||||
//@ts-ignore
|
||||
<div className={actionDesignerCss} ref={buttonRef as React.Ref<HTMLButtonElement>}>
|
||||
{ctx?.params['tree'] && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
@ -63,6 +67,7 @@ export const ExpandAction = (props) => {
|
||||
}}
|
||||
icon={<Icon type={ctx?.expandFlag ? iconCollapse : iconExpand} />}
|
||||
type={props.type}
|
||||
style={props?.style}
|
||||
>
|
||||
{props.children?.[1]}
|
||||
<span style={{ marginLeft: 10 }}>{ctx?.expandFlag ? compile(titleCollapse) : compile(titleExpand)}</span>
|
||||
@ -71,3 +76,5 @@ export const ExpandAction = (props) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandAction = forwardRef<HTMLButtonElement | HTMLAnchorElement, any>(InternalExpandAction);
|
||||
|
@ -62,7 +62,8 @@ export const formSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'FormItem',
|
||||
collectionName: name,
|
||||
@ -103,7 +104,8 @@ export const readPrettyFormSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
insertAdjacentPosition: 'beforeEnd',
|
||||
componentName: 'ReadPrettyFormItem',
|
||||
@ -294,7 +296,8 @@ export const formDetailsSettings = new SchemaSettings({
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
|
||||
const defaultResource =
|
||||
fieldSchema?.['x-decorator-props']?.resource || fieldSchema?.['x-decorator-props']?.association;
|
||||
return {
|
||||
componentName: 'Details',
|
||||
collectionName: name,
|
||||
|
@ -39,7 +39,8 @@ export const GridCardDesigner = () => {
|
||||
const sortFields = useSortFields(name);
|
||||
const record = useRecord();
|
||||
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 columnCountSchema = useMemo(() => {
|
||||
|
@ -2,21 +2,125 @@ import { isValid } from '@formily/shared';
|
||||
import { toFixedByStep } from '@nocobase/utils/client';
|
||||
import type { InputProps } from 'antd/es/input';
|
||||
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) => {
|
||||
const { step, value, addonBefore, addonAfter } = props;
|
||||
if (!isValid(props.value)) {
|
||||
return null;
|
||||
}
|
||||
const result = toFixedByStep(value, step);
|
||||
if (isNaN(result)) {
|
||||
const { step, formatStyle, value, addonBefore, addonAfter, unitConversion, unitConversionType, separator } = props;
|
||||
|
||||
const result = useMemo(() => {
|
||||
return formatNumber({ step, formatStyle, value, unitConversion, unitConversionType, separator });
|
||||
}, [step, formatStyle, value, unitConversion, unitConversionType, separator]);
|
||||
|
||||
if (!isValid(result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'nb-read-pretty-input-number'}>
|
||||
{addonBefore}
|
||||
{result}
|
||||
<span dangerouslySetInnerHTML={{ __html: result }} />
|
||||
{addonAfter}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@nocobase/test/client';
|
||||
import React from 'react';
|
||||
import { formatNumberWithSeparator, formatUnitConversion, scientificNotation } from '../ReadPretty';
|
||||
import App2 from '../demos/addonBefore&addonAfter';
|
||||
import App3 from '../demos/highPrecisionDecimals';
|
||||
import App1 from '../demos/inputNumber';
|
||||
@ -43,7 +44,7 @@ describe('InputNumber: addonBefore/addonAfter', () => {
|
||||
fireEvent.change(input, { target: { value: 1 } });
|
||||
expect(input.value).toBe('1');
|
||||
// @ts-ignore
|
||||
expect(screen.getByText('¥1万元')).toBeInTheDocument();
|
||||
expect(screen.getByText('¥')).toBeInTheDocument();
|
||||
|
||||
// empty value
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
@ -67,7 +68,7 @@ describe('InputNumber: High precision decimals', () => {
|
||||
fireEvent.change(input, { target: { value: 1 } });
|
||||
expect(input.value).toBe('1.00');
|
||||
// @ts-ignore
|
||||
expect(screen.getByText('1.00%')).toBeInTheDocument();
|
||||
expect(screen.getByText('1.00')).toBeInTheDocument();
|
||||
|
||||
// empty value
|
||||
fireEvent.change(input, { target: { value: '' } });
|
||||
@ -75,3 +76,48 @@ describe('InputNumber: High precision decimals', () => {
|
||||
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>');
|
||||
});
|
||||
});
|
||||
|
@ -35,7 +35,8 @@ export const ListDesigner = () => {
|
||||
const sortFields = useSortFields(name);
|
||||
const record = useRecord();
|
||||
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) => {
|
||||
return item.startsWith('-')
|
||||
? {
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
useTableBlockContext,
|
||||
useTableSelectorContext,
|
||||
} from '../../../';
|
||||
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
|
||||
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
|
||||
import { useToken } from '../__builtins__';
|
||||
import { SubFormProvider } from '../association-field/hooks';
|
||||
@ -242,7 +243,8 @@ const usePaginationProps = (pagination1, pagination2) => {
|
||||
return result.total <= result.pageSize ? false : result;
|
||||
};
|
||||
|
||||
export const Table: any = observer(
|
||||
export const Table: any = withDynamicSchemaProps(
|
||||
observer(
|
||||
(props: {
|
||||
useProps?: () => any;
|
||||
onChange?: (pagination, filters, sorter, extra) => void;
|
||||
@ -259,8 +261,11 @@ export const Table: any = observer(
|
||||
isSubTable?: boolean;
|
||||
}) => {
|
||||
const { token } = useToken();
|
||||
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
|
||||
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
|
||||
const { pagination: pagination1, useProps, ...others1 } = props;
|
||||
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { pagination: pagination2, ...others2 } = useProps?.() || {};
|
||||
|
||||
const {
|
||||
dragSort = false,
|
||||
showIndex = true,
|
||||
@ -270,6 +275,7 @@ export const Table: any = observer(
|
||||
rowKey,
|
||||
required,
|
||||
onExpand,
|
||||
onClickRow,
|
||||
...others
|
||||
} = { ...others1, ...others2 } as any;
|
||||
const field = useArrayField(others);
|
||||
@ -566,6 +572,10 @@ export const Table: any = observer(
|
||||
className={css`
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
.ant-table-thead {
|
||||
.ant-table-cell {
|
||||
text-align: center;
|
||||
}
|
||||
.ant-table-wrapper {
|
||||
height: 100%;
|
||||
.ant-spin-nested-loading {
|
||||
@ -574,11 +584,6 @@ export const Table: any = observer(
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.ant-table-thead {
|
||||
.ant-table-cell {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -627,5 +632,6 @@ export const Table: any = observer(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
),
|
||||
{ displayName: 'Table' },
|
||||
);
|
||||
|
@ -78,7 +78,8 @@ export const TableBlockDesigner = () => {
|
||||
const { dn } = useDesignable();
|
||||
|
||||
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 sort = defaultSort?.map((item: string) => {
|
||||
return item?.startsWith('-')
|
||||
|
@ -2,7 +2,8 @@ import { DownOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
|
||||
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 { useACLRolesCheck, useRecordPkValue } from '../../acl/ACLProvider';
|
||||
import {
|
||||
@ -96,8 +97,7 @@ function useAclCheckFn() {
|
||||
return actionAclCheck;
|
||||
}
|
||||
|
||||
export const CreateRecordAction = observer(
|
||||
(props: any) => {
|
||||
const InternalCreateRecordAction = (props: any, ref) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const collection = useCollection_deprecated();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -125,8 +125,11 @@ export const CreateRecordAction = observer(
|
||||
});
|
||||
});
|
||||
}, [field, linkageRules, localVariables, variables]);
|
||||
const internalRef = createRef<HTMLButtonElement | HTMLAnchorElement>();
|
||||
const buttonRef = composeRef(ref, internalRef);
|
||||
return (
|
||||
<div className={actionDesignerCss}>
|
||||
//@ts-ignore
|
||||
<div className={actionDesignerCss} ref={buttonRef as React.Ref<HTMLButtonElement>}>
|
||||
<ActionContextProvider value={{ ...ctx, visible, setVisible }}>
|
||||
<CreateAction
|
||||
{...props}
|
||||
@ -142,9 +145,7 @@ export const CreateRecordAction = observer(
|
||||
</ActionContextProvider>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ displayName: 'CreateRecordAction' },
|
||||
);
|
||||
};
|
||||
|
||||
function getLinkageCollection(str, form, field) {
|
||||
const variablesCtx = { $form: form.values, $iteration: form.values };
|
||||
@ -283,6 +284,7 @@ function FinallyButton({
|
||||
designable: boolean;
|
||||
}) {
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
|
||||
if (inheritsCollections?.length > 0) {
|
||||
if (!linkageFromForm) {
|
||||
return allowAddToCurrent === undefined || allowAddToCurrent ? (
|
||||
@ -360,6 +362,7 @@ function FinallyButton({
|
||||
onClick?.(collection);
|
||||
}}
|
||||
style={{
|
||||
...props?.style,
|
||||
display: !designable && field?.data?.hidden && 'none',
|
||||
opacity: designable && field?.data?.hidden && 0.1,
|
||||
}}
|
||||
@ -368,3 +371,5 @@ function FinallyButton({
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export const CreateRecordAction = forwardRef<HTMLButtonElement | HTMLAnchorElement, any>(InternalCreateRecordAction);
|
||||
|
@ -10,6 +10,7 @@ export const CreateFilterActionInitializer = (props) => {
|
||||
'x-designer': 'Action.Designer',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
useProps: '{{ useFilterBlockActionProps }}',
|
||||
},
|
||||
};
|
||||
|
@ -3,8 +3,9 @@ import { TableOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useCollectionManager_deprecated } from '../../collection-manager';
|
||||
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||
import { createTableBlockSchema, useRecordCollectionDataSourceItems } from '../utils';
|
||||
import { useRecordCollectionDataSourceItems } from '../utils';
|
||||
import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application';
|
||||
import { createTableBlockUISchema } from '../../modules/blocks/data-blocks/table/createTableBlockUISchema';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
@ -17,7 +18,7 @@ export const RecordAssociationBlockInitializer = () => {
|
||||
const { getCollection } = useCollectionManager_deprecated();
|
||||
const field = itemConfig.field;
|
||||
const collection = getCollection(field.target);
|
||||
const resource = `${field.collectionName}.${field.name}`;
|
||||
const association = `${field.collectionName}.${field.name}`;
|
||||
return (
|
||||
<SchemaInitializerItem
|
||||
icon={<TableOutlined />}
|
||||
@ -28,17 +29,15 @@ export const RecordAssociationBlockInitializer = () => {
|
||||
insert(s);
|
||||
} else {
|
||||
insert(
|
||||
createTableBlockSchema({
|
||||
createTableBlockUISchema({
|
||||
rowKey: collection.filterTargetKey,
|
||||
collection: field.target,
|
||||
dataSource: collection.dataSource,
|
||||
resource,
|
||||
association: resource,
|
||||
association: association,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}}
|
||||
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);
|
||||
|
||||
insert(
|
||||
createTableBlockSchema({
|
||||
createTableBlockUISchema({
|
||||
rowKey: collection.filterTargetKey,
|
||||
collection: field.target,
|
||||
dataSource: collection.dataSource,
|
||||
association: `${field.collectionName}.${field.name}`,
|
||||
}),
|
||||
|
@ -1181,8 +1181,6 @@ export const createFormBlockSchema = (options) => {
|
||||
resource: resourceName,
|
||||
collection,
|
||||
association,
|
||||
// action: 'get',
|
||||
// useParams: '{{ useParamsFromRecord }}',
|
||||
},
|
||||
'x-toolbar': 'BlockSchemaToolbar',
|
||||
...(settings ? { 'x-settings': settings } : { 'x-designer': designer }),
|
||||
@ -1357,6 +1355,12 @@ export const createReadPrettyFormBlockSchema = (options) => {
|
||||
return schema;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* 已弃用,可以使用 createTableBlockUISchema 替换
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
export const createTableBlockSchema = (options) => {
|
||||
const {
|
||||
collection,
|
||||
@ -1688,6 +1692,15 @@ function useAssociationFields({
|
||||
const targetCollection = cm.getCollection(field.target);
|
||||
const title = `${compile(field.uiSchema.title || field.name)} -> ${compile(targetCollection.title)}`;
|
||||
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 (
|
||||
componentName &&
|
||||
template.componentName === componentName &&
|
||||
|
@ -56,13 +56,22 @@ export interface GeneralSchemaDesignerProps {
|
||||
* @default true
|
||||
*/
|
||||
draggable?: boolean;
|
||||
showDataSource?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `SchemaToolbar` instead
|
||||
*/
|
||||
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 field = useField();
|
||||
const { t } = useTranslation();
|
||||
@ -112,7 +121,9 @@ export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any
|
||||
<div className={classNames('general-schema-designer-title', titleCss)}>
|
||||
<Space size={2}>
|
||||
<span className={'title-tag'}>
|
||||
{dataSource ? `${compile(dataSource?.displayName)} > ${compile(title)}` : compile(title)}
|
||||
{showDataSource && dataSource
|
||||
? `${compile(dataSource?.displayName)} > ${compile(title)}`
|
||||
: compile(title)}
|
||||
</span>
|
||||
{template && (
|
||||
<span className={'title-tag'}>
|
||||
|
@ -293,7 +293,7 @@ export const SchemaSettingsTemplate = function Template(props) {
|
||||
|
||||
const findGridSchema = (fieldSchema) => {
|
||||
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) => {
|
||||
if (s['x-component'] === 'Grid' || s['x-component'] === 'BlockTemplate') {
|
||||
return s;
|
||||
@ -310,7 +310,7 @@ const findGridSchema = (fieldSchema) => {
|
||||
|
||||
const findBlockTemplateSchema = (fieldSchema) => {
|
||||
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) => {
|
||||
if (s['x-component'] === 'BlockTemplate') {
|
||||
return s;
|
||||
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -45,6 +45,7 @@ import { selectComponentFieldSettings } from '../modules/fields/component/Select
|
||||
import { subTablePopoverComponentFieldSettings } from '../modules/fields/component/SubTable/subTablePopoverComponentFieldSettings';
|
||||
import { tagComponentFieldSettings } from '../modules/fields/component/Tag/tagComponentFieldSettings';
|
||||
import { unixTimestampComponentFieldSettings } from '../modules/fields/component/UnixTimestamp/unixTimestampComponentFieldSettings';
|
||||
import { inputNumberComponentFieldSettings } from '../modules/fields/component/InputNumber/inputNumberComponentFieldSettings';
|
||||
|
||||
export class SchemaSettingsPlugin extends Plugin {
|
||||
async load() {
|
||||
@ -95,6 +96,7 @@ export class SchemaSettingsPlugin extends Plugin {
|
||||
this.schemaSettingsManager.add(subTablePopoverComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(datePickerComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(unixTimestampComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(inputNumberComponentFieldSettings);
|
||||
|
||||
this.schemaSettingsManager.add(fileManagerComponentFieldSettings);
|
||||
this.schemaSettingsManager.add(tagComponentFieldSettings);
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-nocobase-app",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"main": "src/index.js",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/data-source-manager",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"dependencies": {
|
||||
"@nocobase/actions": "0.20.0-alpha.16",
|
||||
"@nocobase/cache": "0.20.0-alpha.16",
|
||||
"@nocobase/database": "0.20.0-alpha.16",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/actions": "0.20.0-alpha.17",
|
||||
"@nocobase/cache": "0.20.0-alpha.17",
|
||||
"@nocobase/database": "0.20.0-alpha.17",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"jsonwebtoken": "^8.5.1"
|
||||
},
|
||||
|
@ -7,3 +7,10 @@ export function parseCollectionName(collection: string) {
|
||||
const dataSourceName = dataSourceCollection[0] ?? 'main';
|
||||
return [dataSourceName, collectionName];
|
||||
}
|
||||
|
||||
export function joinCollectionName(dataSourceName: string, collectionName: string) {
|
||||
if (!dataSourceName || dataSourceName === 'main') {
|
||||
return collectionName;
|
||||
}
|
||||
return `${dataSourceName}:${collectionName}`;
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/database",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@nocobase/logger": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/logger": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"async-mutex": "^0.3.2",
|
||||
"chalk": "^4.1.1",
|
||||
"cron-parser": "4.4.0",
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
@ -14,6 +14,36 @@ describe('belongs to field', () => {
|
||||
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 () => {
|
||||
const User = db.collection({
|
||||
name: 'users',
|
||||
|
@ -27,7 +27,7 @@ import { CollectionFactory } from './collection-factory';
|
||||
import { CollectionGroupManager } from './collection-group-manager';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
import DatabaseUtils from './database-utils';
|
||||
import ReferencesMap from './features/ReferencesMap';
|
||||
import ReferencesMap from './features/references-map';
|
||||
import { referentialIntegrityCheck } from './features/referential-integrity-check';
|
||||
import { ArrayFieldRepository } from './field-repository/array-field-repository';
|
||||
import * as FieldTypes from './fields';
|
||||
|
@ -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;
|
107
packages/core/database/src/features/references-map.ts
Normal file
107
packages/core/database/src/features/references-map.ts
Normal 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;
|
@ -21,6 +21,11 @@ export async function referentialIntegrityCheck(options: ReferentialIntegrityChe
|
||||
|
||||
for (const reference of references) {
|
||||
const { sourceCollectionName, sourceField, targetField, onDelete } = reference;
|
||||
|
||||
if (onDelete === 'NO ACTION') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sourceCollection = db.collections.get(sourceCollectionName);
|
||||
const sourceRepository = sourceCollection.repository;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import lodash, { omit } from 'lodash';
|
||||
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 { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -16,20 +16,26 @@ export class BelongsToField extends RelationField {
|
||||
return target || Utils.pluralize(name);
|
||||
}
|
||||
|
||||
static toReference(db, association, onDelete) {
|
||||
static toReference(db, association, onDelete, priority: ReferencePriority = 'default'): Reference {
|
||||
const targetKey = association.targetKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
sourceCollectionName: db.modelCollection.get(association.source).name,
|
||||
sourceField: association.foreignKey,
|
||||
targetField: targetKey,
|
||||
targetCollectionName: db.modelCollection.get(association.target).name,
|
||||
onDelete: onDelete,
|
||||
};
|
||||
priority: priority,
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { AssociationScope, BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BelongsToField } from './belongs-to-field';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
@ -32,6 +32,8 @@ export class BelongsToManyField extends RelationField {
|
||||
|
||||
const onDelete = this.options.onDelete || 'CASCADE';
|
||||
|
||||
const priority = this.options.onDelete ? 'user' : 'default';
|
||||
|
||||
const targetAssociation = association.toTarget;
|
||||
|
||||
if (association.targetKey) {
|
||||
@ -45,8 +47,8 @@ export class BelongsToManyField extends RelationField {
|
||||
}
|
||||
|
||||
return [
|
||||
BelongsToField.toReference(db, targetAssociation, onDelete),
|
||||
BelongsToField.toReference(db, sourceAssociation, onDelete),
|
||||
BelongsToField.toReference(db, targetAssociation, onDelete, priority),
|
||||
BelongsToField.toReference(db, sourceAssociation, onDelete, priority),
|
||||
];
|
||||
}
|
||||
|
||||
@ -149,10 +151,6 @@ export class BelongsToManyField extends RelationField {
|
||||
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||
}
|
||||
|
||||
if (!this.options.onDelete) {
|
||||
this.options.onDelete = 'CASCADE';
|
||||
}
|
||||
|
||||
const belongsToManyOptions = {
|
||||
constraints: false,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { buildReference, Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -89,13 +89,13 @@ export class HasManyField extends RelationField {
|
||||
reference(association): Reference {
|
||||
const sourceKey = association.sourceKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||
sourceField: association.foreignKey,
|
||||
targetField: sourceKey,
|
||||
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||
onDelete: this.options.onDelete,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checkAssociationKeys() {
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { buildReference, Reference } from '../features/references-map';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
@ -98,13 +98,13 @@ export class HasOneField extends RelationField {
|
||||
reference(association): Reference {
|
||||
const sourceKey = association.sourceKey;
|
||||
|
||||
return {
|
||||
return buildReference({
|
||||
sourceCollectionName: this.database.modelCollection.get(association.target).name,
|
||||
sourceField: association.foreignKey,
|
||||
targetField: sourceKey,
|
||||
targetCollectionName: this.database.modelCollection.get(association.source).name,
|
||||
onDelete: this.options.onDelete,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
checkAssociationKeys() {
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/devtools",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@nocobase/build": "0.20.0-alpha.16",
|
||||
"@nocobase/client": "0.20.0-alpha.16",
|
||||
"@nocobase/test": "0.20.0-alpha.16",
|
||||
"@nocobase/build": "0.20.0-alpha.17",
|
||||
"@nocobase/client": "0.20.0-alpha.17",
|
||||
"@nocobase/test": "0.20.0-alpha.17",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-bodyparser": "^4.3.4",
|
||||
"@types/lodash": "^4.14.177",
|
||||
|
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nocobase/evaluators",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formulajs/formulajs": "4.2.0",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"mathjs": "^10.6.0"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/logger",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "nocobase logging library",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nocobase/resourcer",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "",
|
||||
"main": "./lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"deepmerge": "^4.2.2",
|
||||
"koa-compose": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -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 _ from 'lodash';
|
||||
import Middleware, { MiddlewareType } from './middleware';
|
||||
import Resource from './resource';
|
||||
import { HandlerType } from './resourcer';
|
||||
import { RecordableHistogram, performance } from 'perf_hooks';
|
||||
|
||||
export type ActionType = string | HandlerType | ActionOptions;
|
||||
|
||||
@ -161,27 +160,38 @@ export interface ActionParams {
|
||||
*/
|
||||
values?: any;
|
||||
/**
|
||||
* 当前资源的主体,对应的表名或 Model 名称
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link action.resourceName.split(',')[0]} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
resourceName?: string;
|
||||
/**
|
||||
* 资源标识符
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link filterByTk} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
resourceIndex?: string;
|
||||
/**
|
||||
* 资源的从属关系
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link action.resourceName.split(',')[1]} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
associatedName?: string;
|
||||
/**
|
||||
* 从属关系的标识符
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link action.sourceId} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
associatedIndex?: string;
|
||||
/**
|
||||
* 从属关系的当前实例
|
||||
* This method is deprecated and should not be used.
|
||||
* @deprecated
|
||||
*/
|
||||
associated?: any;
|
||||
/**
|
||||
* 资源提供哪些行为或方法
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link action.actionName} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
actionName?: string;
|
||||
/**
|
||||
@ -204,11 +214,23 @@ export class Action {
|
||||
public params: ActionParams = {};
|
||||
|
||||
public actionName: string;
|
||||
|
||||
public resourceName: string;
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link this.sourceId} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
public resourceOf: any;
|
||||
|
||||
public sourceId: any;
|
||||
|
||||
public readonly middlewares: Array<Middleware> = [];
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(options: ActionOptions) {
|
||||
options = requireModule(options);
|
||||
if (typeof options === 'function') {
|
||||
@ -221,15 +243,22 @@ export class Action {
|
||||
this.mergeParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
actionName: this.actionName,
|
||||
resourceName: this.resourceName,
|
||||
resourceOf: this.resourceOf,
|
||||
resourceOf: this.sourceId,
|
||||
sourceId: this.sourceId,
|
||||
params: this.params,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
clone() {
|
||||
const options = _.cloneDeep(this.options);
|
||||
delete options.middleware;
|
||||
@ -241,6 +270,9 @@ export class Action {
|
||||
return action;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setContext(context: any) {
|
||||
this.context = context;
|
||||
}
|
||||
@ -266,34 +298,55 @@ export class Action {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setResource(resource: Resource) {
|
||||
this.resource = resource;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getResource() {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getOptions(): ActionOptions {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setName(name: ActionName) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getMiddlewareHandlers() {
|
||||
return this.middlewares
|
||||
.filter((middleware) => middleware.canAccess(this.name))
|
||||
.map((middleware) => middleware.getHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getHandler() {
|
||||
const handler = requireModule(this.handler || this.resource.resourcer.getRegisteredHandler(this.name));
|
||||
if (typeof handler !== 'function') {
|
||||
@ -303,6 +356,9 @@ export class Action {
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getHandlers() {
|
||||
const handlers = [
|
||||
...this.resource.resourcer.getMiddlewares(),
|
||||
@ -315,10 +371,16 @@ export class Action {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async execute(context: any, next?: any) {
|
||||
return await compose(this.getHandlers())(context, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static toInstanceMap(actions: object, resource?: Resource) {
|
||||
return new Map(
|
||||
Object.entries(actions).map(([key, options]) => {
|
||||
|
@ -150,6 +150,9 @@ export interface ImportOptions {
|
||||
}
|
||||
|
||||
export class Resourcer {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public readonly options: ResourcerOptions;
|
||||
protected resources = new Map<string, Resource>();
|
||||
/**
|
||||
@ -173,6 +176,7 @@ export class Resourcer {
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.directory] 指定配置所在路径
|
||||
* @param {array} [options.extensions = ['js', 'ts', 'json']] 文件后缀
|
||||
*
|
||||
*/
|
||||
public async import(options: ImportOptions): Promise<Map<string, Resource>> {
|
||||
const { extensions = ['js', 'ts', 'json'], directory } = options;
|
||||
@ -206,14 +210,27 @@ export class Resourcer {
|
||||
return this.resources.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
removeResource(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) {
|
||||
this.registerActionHandler(name, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link this.registerActionHandlers()} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
registerActions(handlers: Handlers) {
|
||||
this.registerActionHandlers(handlers);
|
||||
}
|
||||
@ -233,14 +250,23 @@ export class Resourcer {
|
||||
this.actionHandlers.set(name, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getRegisteredHandler(name: ActionName) {
|
||||
return this.actionHandlers.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getRegisteredHandlers() {
|
||||
return this.actionHandlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getResource(name: string): Resource {
|
||||
if (!this.resources.has(name)) {
|
||||
throw new Error(`${name} resource does not exist`);
|
||||
@ -248,6 +274,9 @@ export class Resourcer {
|
||||
return this.resources.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getAction(name: string, action: ActionName): Action {
|
||||
// 支持注册局部 action
|
||||
if (this.actionHandlers.has(`${name}:${action}`)) {
|
||||
@ -256,6 +285,9 @@ export class Resourcer {
|
||||
return this.getResource(name).getAction(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getMiddlewares() {
|
||||
return this.middlewares.nodes;
|
||||
}
|
||||
@ -264,6 +296,11 @@ export class Resourcer {
|
||||
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 = {}) {
|
||||
return async (ctx: ResourcerContext, next: () => Promise<any>) => {
|
||||
if (skipIfDataSourceExists) {
|
||||
@ -317,6 +354,7 @@ export class Resourcer {
|
||||
|
||||
ctx.action.setContext(ctx);
|
||||
ctx.action.actionName = params.actionName;
|
||||
ctx.action.sourceId = params.associatedIndex;
|
||||
ctx.action.resourceOf = params.associatedIndex;
|
||||
ctx.action.resourceName = params.associatedName
|
||||
? `${params.associatedName}.${params.resourceName}`
|
||||
@ -349,11 +387,7 @@ export class Resourcer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 实验性 API
|
||||
*
|
||||
* @param options
|
||||
* @param context
|
||||
* @param next
|
||||
* @internal
|
||||
*/
|
||||
async execute(options: ExecuteOptions, context: ResourcerContext = {}, next?: any) {
|
||||
const { resource, action } = options;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/sdk",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"license": "Apache-2.0",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/server",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"main": "lib/index.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "Apache-2.0",
|
||||
@ -10,18 +10,18 @@
|
||||
"@koa/cors": "^3.1.0",
|
||||
"@koa/multer": "^3.0.2",
|
||||
"@koa/router": "^9.4.0",
|
||||
"@nocobase/acl": "0.20.0-alpha.16",
|
||||
"@nocobase/actions": "0.20.0-alpha.16",
|
||||
"@nocobase/auth": "0.20.0-alpha.16",
|
||||
"@nocobase/cache": "0.20.0-alpha.16",
|
||||
"@nocobase/data-source-manager": "0.20.0-alpha.16",
|
||||
"@nocobase/database": "0.20.0-alpha.16",
|
||||
"@nocobase/evaluators": "0.20.0-alpha.16",
|
||||
"@nocobase/logger": "0.20.0-alpha.16",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.16",
|
||||
"@nocobase/sdk": "0.20.0-alpha.16",
|
||||
"@nocobase/telemetry": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/acl": "0.20.0-alpha.17",
|
||||
"@nocobase/actions": "0.20.0-alpha.17",
|
||||
"@nocobase/auth": "0.20.0-alpha.17",
|
||||
"@nocobase/cache": "0.20.0-alpha.17",
|
||||
"@nocobase/data-source-manager": "0.20.0-alpha.17",
|
||||
"@nocobase/database": "0.20.0-alpha.17",
|
||||
"@nocobase/evaluators": "0.20.0-alpha.17",
|
||||
"@nocobase/logger": "0.20.0-alpha.17",
|
||||
"@nocobase/resourcer": "0.20.0-alpha.17",
|
||||
"@nocobase/sdk": "0.20.0-alpha.17",
|
||||
"@nocobase/telemetry": "0.20.0-alpha.17",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"@types/decompress": "4.2.4",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/koa-send": "^4.1.3",
|
||||
|
@ -142,19 +142,41 @@ export type MaintainingCommandStatus = {
|
||||
};
|
||||
|
||||
export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
declare middleware: any;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
stopped = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
ready = false;
|
||||
declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public rawOptions: ApplicationOptions;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public activatedCommand: {
|
||||
name: string;
|
||||
} = null;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public running = false;
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public perfHistograms = new Map<string, RecordableHistogram>();
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
||||
protected _started: boolean;
|
||||
protected _logger: SystemLogger;
|
||||
private _authenticated = false;
|
||||
private _maintaining = false;
|
||||
private _maintainingCommandStatus: MaintainingCommandStatus;
|
||||
@ -193,12 +215,18 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
protected _loaded: boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get loaded() {
|
||||
return this._loaded;
|
||||
}
|
||||
|
||||
private _maintainingMessage: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
get maintainingMessage() {
|
||||
return this._maintainingMessage;
|
||||
}
|
||||
@ -222,8 +250,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this.mainDataSource.collectionManager.db;
|
||||
}
|
||||
|
||||
protected _logger: SystemLogger;
|
||||
|
||||
get logger() {
|
||||
return this._logger;
|
||||
}
|
||||
@ -244,6 +270,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
set cache(cache: Cache) {
|
||||
this._cache = cache;
|
||||
}
|
||||
@ -278,6 +307,11 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
protected _locales: Locale;
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link #localeManager} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
get locales() {
|
||||
return this._locales;
|
||||
}
|
||||
@ -312,10 +346,16 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._dataSourceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getMaintaining() {
|
||||
return this._maintainingCommandStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setMaintaining(_maintainingCommandStatus: MaintainingCommandStatus) {
|
||||
this._maintainingCommandStatus = _maintainingCommandStatus;
|
||||
|
||||
@ -329,6 +369,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
this._maintaining = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setMaintainingMessage(message: string) {
|
||||
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() {
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link #this.pm.addPreset()} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
plugin<O = any>(pluginClass: any, options?: O) {
|
||||
@ -359,6 +409,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
callback() {
|
||||
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) {
|
||||
return this.db.collection(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is deprecated and should not be used.
|
||||
* Use {@link #this.resourcer.define()} instead.
|
||||
* @deprecated
|
||||
*/
|
||||
resource(options: ResourceOptions) {
|
||||
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) {
|
||||
return this.resourcer.registerActions(handlers);
|
||||
}
|
||||
@ -392,11 +460,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return (this.cli as any)._findCommand(name);
|
||||
}
|
||||
|
||||
async preload() {
|
||||
// load core collections
|
||||
// load plugin commands
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async reInit() {
|
||||
if (!this._loaded) {
|
||||
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
|
||||
*/
|
||||
getPlugin<P extends Plugin>(name: string | typeof Plugin) {
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
|
||||
createCli() {
|
||||
protected createCLI() {
|
||||
const command = new AppCommand('nocobase')
|
||||
.usage('[command] [options]')
|
||||
.hook('preAction', async (_, actionCommand) => {
|
||||
@ -568,6 +641,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadMigrations(options) {
|
||||
const { directory, context, namespace } = options;
|
||||
const migrations = {
|
||||
@ -594,6 +670,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadCoreMigrations() {
|
||||
const migrations = await this.loadMigrations({
|
||||
directory: resolve(__dirname, 'migrations'),
|
||||
@ -624,11 +703,17 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadPluginCommands() {
|
||||
this.log.debug('load plugin commands');
|
||||
await this.pm.loadCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async runAsCLI(
|
||||
argv = process.argv,
|
||||
options?: ParseOptions & { throwError?: boolean; reqId?: string },
|
||||
@ -732,6 +817,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
this.stopped = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async emitStartedEvent(options: StartOptions = {}) {
|
||||
await this.emitAsync('__started', this, {
|
||||
maintainingStatus: lodash.cloneDeep(this._maintainingCommandStatus),
|
||||
@ -743,6 +831,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._started;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async tryReloadOrRestart(options: StartOptions = {}) {
|
||||
if (this._started) {
|
||||
await this.restart(options);
|
||||
@ -941,6 +1032,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
reInitEvents() {
|
||||
for (const eventName of this.eventNames()) {
|
||||
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._cli = this.createCli();
|
||||
this._cli = this.createCLI();
|
||||
this._i18n = createI18n(options);
|
||||
this.context.db = this.db;
|
||||
|
||||
|
@ -67,6 +67,9 @@ export class Locale {
|
||||
|
||||
async getCacheResources(lang: string) {
|
||||
this.resourceCached.set(lang, true);
|
||||
if (process.env.APP_ENV !== 'production') {
|
||||
await this.cache.reset();
|
||||
}
|
||||
return await this.wrapCache(`resources:${lang}`, () => this.getResources(lang));
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,10 @@ export const getResource = (packageName: string, lang: string, isPlugin = true)
|
||||
for (const prefix of prefixes) {
|
||||
try {
|
||||
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);
|
||||
resources.push(resource);
|
||||
} catch (error) {
|
||||
|
@ -3,12 +3,21 @@ import lodash from 'lodash';
|
||||
import { PluginManager } from './plugin-manager';
|
||||
|
||||
export class PluginManagerRepository extends Repository {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
pm: PluginManager;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
setPluginManager(pm: PluginManager) {
|
||||
this.pm = pm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
async remove(name: string | string[]) {
|
||||
await this.destroy({
|
||||
filter: {
|
||||
@ -17,6 +26,9 @@ export class PluginManagerRepository extends Repository {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
async enable(name: string | string[]) {
|
||||
const pluginNames = lodash.castArray(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[]) {
|
||||
name = lodash.cloneDeep(name);
|
||||
|
||||
|
@ -45,12 +45,39 @@ export interface InstallOptions {
|
||||
export class AddPresetError extends Error {}
|
||||
|
||||
export class PluginManager {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
app: Application;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
collection: Collection;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
pluginInstances = new Map<typeof Plugin, Plugin>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
pluginAliases = new Map<string, Plugin>();
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
server: net.Server;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
_repository: PluginManagerRepository;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(public options: PluginManagerOptions) {
|
||||
this.app = options.app;
|
||||
this.app.db.registerRepositories({
|
||||
@ -76,18 +103,22 @@ export class PluginManager {
|
||||
this.app.resourcer.use(uploadMiddleware);
|
||||
}
|
||||
|
||||
_repository: PluginManagerRepository;
|
||||
|
||||
get repository() {
|
||||
return this.app.db.getRepository('applicationPlugins') as PluginManagerRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static async getPackageJson(packageName: string) {
|
||||
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' });
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static async getPackageName(name: string) {
|
||||
const prefixes = this.getPluginPkgPrefix();
|
||||
for (const prefix of prefixes) {
|
||||
@ -100,12 +131,18 @@ export class PluginManager {
|
||||
throw new Error(`${name} plugin does not exist`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static getPluginPkgPrefix() {
|
||||
return (process.env.PLUGIN_PACKAGE_PREFIX || '@nocobase/plugin-,@nocobase/preset-,@nocobase/plugin-pro-').split(
|
||||
',',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static async findPackage(name: string) {
|
||||
try {
|
||||
const packageName = this.getPackageName(name);
|
||||
@ -130,6 +167,9 @@ export class PluginManager {
|
||||
throw new Error(`No available packages found, ${name} plugin does not exist`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static clearCache(packageName: string) {
|
||||
return;
|
||||
const packageNamePath = packageName.replace('/', sep);
|
||||
@ -140,6 +180,9 @@ export class PluginManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
static async resolvePlugin(pluginName: string | typeof Plugin, isUpgrade = false, isPkg = false) {
|
||||
if (typeof pluginName === 'string') {
|
||||
const packageName = isPkg ? pluginName : await this.getPackageName(pluginName);
|
||||
@ -296,11 +339,17 @@ export class PluginManager {
|
||||
await instance.afterAdd();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async initPlugins() {
|
||||
await this.initPresetPlugins();
|
||||
await this.initOtherPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadCommands() {
|
||||
this.app.log.debug('load commands');
|
||||
const items = await this.repository.find({
|
||||
@ -619,6 +668,9 @@ export class PluginManager {
|
||||
await execa('yarn', ['nocobase', 'refresh']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
async loadOne(plugin: Plugin) {
|
||||
this.app.setMaintainingMessage(`loading plugin ${plugin.name}...`);
|
||||
if (plugin.state.loaded || !plugin.enabled) {
|
||||
@ -637,6 +689,9 @@ export class PluginManager {
|
||||
this.app.setMaintainingMessage(`loaded plugin ${plugin.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async addViaCLI(urlOrName: string, options?: PluginData) {
|
||||
if (isURL(urlOrName)) {
|
||||
await this.addByCompressedFileUrl({
|
||||
@ -679,6 +734,9 @@ export class PluginManager {
|
||||
await execa('yarn', ['nocobase', 'postinstall']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async addByNpm(options: { packageName: string; name?: string; registry: string; authToken?: string }) {
|
||||
let { name = '', registry, packageName, authToken } = options;
|
||||
name = name.trim();
|
||||
@ -693,6 +751,9 @@ export class PluginManager {
|
||||
return this.addByCompressedFileUrl({ name, compressedFileUrl, registry, authToken, type: 'npm' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async addByFile(options: { file: string; registry?: string; authToken?: string; type?: string; name?: string }) {
|
||||
const { file, authToken } = options;
|
||||
|
||||
@ -708,6 +769,9 @@ export class PluginManager {
|
||||
return this.add(name, { packageName }, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async addByCompressedFileUrl(options: {
|
||||
compressedFileUrl: string;
|
||||
registry?: string;
|
||||
@ -748,6 +812,9 @@ export class PluginManager {
|
||||
await this.app.upgrade();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async upgradeByNpm(values: PluginData) {
|
||||
const name = values.name;
|
||||
const plugin = this.get(name);
|
||||
@ -769,6 +836,9 @@ export class PluginManager {
|
||||
return this.upgradeByCompressedFileUrl({ compressedFileUrl, name, version, registry, authToken });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async upgradeByCompressedFileUrl(options: PluginData) {
|
||||
const { name, compressedFileUrl, authToken } = options;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
getNameByPackageName(packageName: string) {
|
||||
const prefixes = PluginManager.getPluginPkgPrefix();
|
||||
const prefix = prefixes.find((prefix) => packageName.startsWith(prefix));
|
||||
@ -808,12 +881,18 @@ export class PluginManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async getNpmVersionList(name: string) {
|
||||
const plugin = this.get(name);
|
||||
const npmInfo = await getNpmInfo(plugin.options.packageName, plugin.options.registry, plugin.options.authToken);
|
||||
return Object.keys(npmInfo.versions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadPresetMigrations() {
|
||||
const migrations = {
|
||||
beforeLoad: [],
|
||||
@ -854,6 +933,9 @@ export class PluginManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadOtherMigrations() {
|
||||
const migrations = {
|
||||
beforeLoad: [],
|
||||
@ -897,6 +979,9 @@ export class PluginManager {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async loadPresetPlugins() {
|
||||
await this.initPresetPlugins();
|
||||
await this.load();
|
||||
@ -931,6 +1016,9 @@ export class PluginManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async initOtherPlugins() {
|
||||
if (this['_initOtherPlugins']) {
|
||||
return;
|
||||
@ -939,6 +1027,9 @@ export class PluginManager {
|
||||
this['_initOtherPlugins'] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async initPresetPlugins() {
|
||||
if (this['_initPresetPlugins']) {
|
||||
return;
|
||||
|
@ -33,9 +33,22 @@ export interface PluginOptions {
|
||||
export abstract class Plugin<O = any> implements PluginInterface {
|
||||
options: any;
|
||||
app: Application;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
model: Model;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
state: any = {};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
private _sourceDir: string;
|
||||
|
||||
constructor(app: Application, options?: any) {
|
||||
this.app = app;
|
||||
this.setOptions(options);
|
||||
@ -80,10 +93,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
return this.options.isPreset;
|
||||
}
|
||||
|
||||
setOptions(options: any) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
getName() {
|
||||
return (this.options as any).name;
|
||||
}
|
||||
@ -92,64 +101,6 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
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() {}
|
||||
|
||||
beforeLoad() {}
|
||||
@ -172,13 +123,86 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
|
||||
async afterRemove() {}
|
||||
|
||||
async importCollections(collectionsPath: string) {
|
||||
// await this.db.import({
|
||||
// directory: collectionsPath,
|
||||
// from: `plugin:${this.getName()}`,
|
||||
// });
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
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() {
|
||||
if (!this.options.packageName) {
|
||||
return;
|
||||
@ -197,6 +221,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
requiredPlugins() {
|
||||
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) });
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected async isDev() {
|
||||
if (!this.options.packageName) {
|
||||
return false;
|
||||
@ -218,6 +248,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
*/
|
||||
async toJSON(options: any = {}) {
|
||||
const { locale = 'en-US' } = options;
|
||||
const { name, packageName, packageJson } = this.options;
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/telemetry",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"description": "nocobase telemetry library",
|
||||
"license": "Apache-2.0",
|
||||
"main": "./lib/index.js",
|
||||
@ -11,7 +11,7 @@
|
||||
"directory": "packages/telemetry"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/utils": "0.20.0-alpha.16",
|
||||
"@nocobase/utils": "0.20.0-alpha.17",
|
||||
"@opentelemetry/api": "^1.7.0",
|
||||
"@opentelemetry/instrumentation": "^0.46.0",
|
||||
"@opentelemetry/resources": "^1.19.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nocobase/test",
|
||||
"version": "0.20.0-alpha.16",
|
||||
"version": "0.20.0-alpha.17",
|
||||
"main": "lib/index.js",
|
||||
"module": "./src/index.ts",
|
||||
"types": "./lib/index.d.ts",
|
||||
@ -40,9 +40,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
@ -56,7 +57,7 @@
|
||||
"sqlite3": "^5.0.8",
|
||||
"supertest": "^6.1.6",
|
||||
"vite": "^5.0.0",
|
||||
"vitest": "^1.0.0",
|
||||
"vitest": "^1.4.0",
|
||||
"vitest-dom": "^0.1.1",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { render } from '@testing-library/react';
|
||||
export { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
function customRender(ui: React.ReactElement, options = {}) {
|
||||
return render(ui, {
|
||||
|
@ -21,7 +21,7 @@ export const defineConfig = (config?: PlaywrightTestConfig) => {
|
||||
forbidOnly: !!process.env.CI,
|
||||
|
||||
// Retry on CI only.
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: 2,
|
||||
|
||||
// Opt out of parallel tests on CI.
|
||||
// workers: process.env.CI ? 1 : undefined,
|
||||
@ -52,7 +52,14 @@ export const defineConfig = (config?: PlaywrightTestConfig) => {
|
||||
},
|
||||
{
|
||||
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'],
|
||||
},
|
||||
],
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user